Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 13693) +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 13694) @@ -1,510 +1,540 @@ function GarrisonHolder() {} GarrisonHolder.prototype.Schema = "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Initialize GarrisonHolder Component */ GarrisonHolder.prototype.Init = function() { // Garrisoned Units this.entities = []; this.spaceOccupied = 0; this.timer = undefined; + this.allowGarrisoning = []; }; /** * Return range at which entities can garrison here */ GarrisonHolder.prototype.GetLoadingRange = function() { var max = +this.template.LoadingRange; return { "max": max, "min": 0 }; }; /** * Return the list of entities garrisoned inside */ GarrisonHolder.prototype.GetEntities = function() { return this.entities; }; /** * Returns an array of unit classes which can be garrisoned inside this * particualar entity. Obtained from the entity's template */ GarrisonHolder.prototype.GetAllowedClassesList = function() { var classes = this.template.List._string; return classes ? classes.split(/\s+/) : []; }; /** * Get Maximum pop which can be garrisoned */ GarrisonHolder.prototype.GetCapacity = function() { return ApplyTechModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity); }; /** * Get the heal rate with which garrisoned units will be healed */ GarrisonHolder.prototype.GetHealRate = function() { return ApplyTechModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity); }; GarrisonHolder.prototype.EjectEntitiesOnDestroy = function() { if (this.template.EjectEntitiesOnDestroy == "true") return true; return false; -} +}; + +/** + * Set this entity to allow or disallow garrisoning in + * Every component calling this function should do it with its own ID, and as long as one + * component doesn't allow this entity to garrison, it can't be garrisoned + * When this entity already contains garrisoned soldiers, + * these will not be able to ungarrison until the flag is set to true again. + * + * This more useful for modern-day features. For example you can't garrison or ungarrison + * a driving vehicle or plane. + */ +GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID) +{ + this.allowGarrisoning[callerID] = allow; +}; + +/** + * Check if no component of this entity blocks garrisoning + * (f.e. because the vehicle is moving too fast) + */ +GarrisonHolder.prototype.IsGarrisoningAllowed = function() +{ + return this.allowGarrisoning.every(function (x) x); +}; /** * Get number of garrisoned units capable of shooting arrows * Not necessarily archers */ GarrisonHolder.prototype.GetGarrisonedArcherCount = function(garrisonArrowClasses) { var count = 0; for each (var entity in this.entities) { var cmpIdentity = Engine.QueryInterface(entity, IID_Identity); var classes = cmpIdentity.GetClassesList(); if (classes.some(function(c){return garrisonArrowClasses.indexOf(c) > -1;})) count++; } return count; }; /** * Checks if an entity can be allowed to garrison in the building * based on its class */ GarrisonHolder.prototype.AllowedToGarrison = function(entity) { + if (!this.IsGarrisoningAllowed()) + return false; + var allowedClasses = this.GetAllowedClassesList(); var entityClasses = (Engine.QueryInterface(entity, IID_Identity)).GetClassesList(); // Check if the unit is allowed to be garrisoned inside the building for each (var allowedClass in allowedClasses) { if (entityClasses.indexOf(allowedClass) != -1) { return true; } } return false; }; /** * Garrison a unit inside. * Returns true if successful, false if not * The timer for AutoHeal is started here */ GarrisonHolder.prototype.Garrison = function(entity) { if (!this.HasEnoughHealth()) return false; // Check if the unit is allowed to be garrisoned inside the building if(!this.AllowedToGarrison(entity)) return false; if (this.GetCapacity() < this.spaceOccupied + 1) return false; var cmpPosition = Engine.QueryInterface(entity, IID_Position); if (!cmpPosition) return false; if (!this.timer && this.GetHealRate() > 0) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } // Actual garrisoning happens here this.entities.push(entity); this.spaceOccupied += 1; cmpPosition.MoveOutOfWorld(); this.UpdateGarrisonFlag(); var cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.PauseProduction(); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {}); return true; }; /** * Simply eject the unit from the garrisoning entity without * moving it * Returns true if successful, false if not */ GarrisonHolder.prototype.Eject = function(entity, forced) { + var entityIndex = this.entities.indexOf(entity); + // Error: invalid entity ID, usually it's already been ejected if (entityIndex == -1) - { // Error: invalid entity ID, usually it's already been ejected return false; // Fail - } // Find spawning location var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); var pos = cmpFootprint.PickSpawnPoint(entity); if (pos.y < 0) { // Error: couldn't find space satisfying the unit's passability criteria if (forced) { // If ejection is forced, we need to continue, so use center of the building var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); pos = cmpPosition.GetPosition(); } else { // Fail return false; } } this.spaceOccupied -= 1; this.entities.splice(entityIndex, 1); var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.Ungarrison(); var cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.UnpauseProduction(); var cmpNewPosition = Engine.QueryInterface(entity, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); // TODO: what direction should they face in? Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {}); return true; }; /** * Order entities to walk to the Rally Point */ GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); if (cmpRallyPoint) { var rallyPos = cmpRallyPoint.GetPositions()[0]; if (rallyPos) { var commands = GetRallyPointCommands(cmpRallyPoint, entities); for each (var com in commands) { ProcessCommand(cmpOwnership.GetOwner(), com); } } } }; /** * Ejects units and orders them to move to the Rally Point. * Returns true if successful, false if not */ GarrisonHolder.prototype.PerformEject = function(entities, forced) { + if (!this.IsGarrisoningAllowed() && !forced) + return false var ejectedEntities = []; var success = true; for each (var entity in entities) { if (this.Eject(entity, forced)) ejectedEntities.push(entity); else success = false; } this.OrderWalkToRallyPoint(ejectedEntities); this.UpdateGarrisonFlag(); return success; }; /** * Unload unit from the garrisoning entity and order them * to move to the Rally Point * Returns true if successful, false if not */ GarrisonHolder.prototype.Unload = function(entity, forced) { return this.PerformEject([entity], forced); }; /** * Unload one or all units that match a template and owner from * the garrisoning entity and order them to move to the Rally Point * Returns true if successful, false if not * * extendedTemplate has the format "p"+ownerid+"&"+template */ GarrisonHolder.prototype.UnloadTemplate = function(extendedTemplate, all, forced) { var index = extendedTemplate.indexOf("&"); if (index == -1) return false; var owner = +extendedTemplate.slice(1,index); var template = extendedTemplate.slice(index+1); var entities = []; var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); for each (var entity in this.entities) { var cmpIdentity = Engine.QueryInterface(entity, IID_Identity); // Units with multiple ranks are grouped together. var name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity); if (name != template) continue; if (owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner()) continue; entities.push(entity); // If 'all' is false, only ungarrison the first matched unit. if (!all) break; } return this.PerformEject(entities, forced); }; /** * Unload all units with same owner as the entity * and order them to move to the Rally Point * Returns true if all successful, false if not */ GarrisonHolder.prototype.UnloadAllOwn = function(forced) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; var owner = cmpOwnership.GetOwner(); // Make copy of entity list var entities = []; for each (var entity in this.entities) { var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == owner) entities.push(entity); } return this.PerformEject(entities, forced); }; /** * Unload all units from the entity * and order them to move to the Rally Point * Returns true if all successful, false if not */ GarrisonHolder.prototype.UnloadAll = function(forced) { var entities = this.entities.slice(0); return this.PerformEject(entities, forced); }; /** * Used to check if the garrisoning entity's health has fallen below * a certain limit after which all garrisoned units are unloaded */ GarrisonHolder.prototype.OnHealthChanged = function(msg) { if (!this.HasEnoughHealth()) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); // Destroy the garrisoned units if the holder kill his entities on destroy or // is not in the world (generally means this holder is inside // a holder which kills its entities which has sunk). if (!this.EjectEntitiesOnDestroy() || !cmpPosition.IsInWorld()) { for each (var entity in this.entities) { var cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth) { cmpHealth.Kill(); } } this.entities = []; Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {}); } else { // Building - force ejection this.UnloadAll(true); } } }; /** * Check if this entity has enough health to garrison units inside it */ GarrisonHolder.prototype.HasEnoughHealth = function() { var cmpHealth = Engine.QueryInterface(this.entity, IID_Health) var hitpoints = cmpHealth.GetHitpoints(); var maxHitpoints = cmpHealth.GetMaxHitpoints(); var ejectHitpoints = Math.floor((+this.template.EjectHealth) * maxHitpoints); return hitpoints > ejectHitpoints; }; /** * Called every second. Heals garrisoned units */ GarrisonHolder.prototype.HealTimeout = function(data) { if (this.entities.length == 0) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } else { for each (var entity in this.entities) { var cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth) { // We do not want to heal unhealable units if (!cmpHealth.IsUnhealable()) cmpHealth.Increase(this.GetHealRate()); } } var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } }; GarrisonHolder.prototype.UpdateGarrisonFlag = function() { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation("garrisoned", true, 0, ""); // TODO: ought to extend ICmpVisual to let us just select variant // keywords without changing the animation too if (this.entities.length) cmpVisual.SelectAnimation("garrisoned", false, 1.0, ""); else cmpVisual.SelectAnimation("idle", false, 1.0, ""); }; /** * Cancel timer when destroyed */ GarrisonHolder.prototype.OnDestroy = function() { if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /** * If a garrisoned entity is captured, or about to be killed (so its owner * changes to '-1'), remove it from the building so we only ever contain valid * entities */ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg) { var entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { // If the entity is dead, remove it directly instead of ejecting the corpse var cmpHealth = Engine.QueryInterface(msg.entity, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() == 0) { this.entities.splice(entityIndex, 1); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {}); } else { // We have to be careful of our passability // ships: not land passable, assume unit was thrown overboard or something // building: land passable, unit can be ejected freely var classes = (Engine.QueryInterface(this.entity, IID_Identity)).GetClassesList(); if (classes.indexOf("Ship") != -1) { // Ship - kill unit var cmpHealth = Engine.QueryInterface(msg.entity, IID_Health); if (cmpHealth) { cmpHealth.Kill(); } this.entities.splice(entityIndex, 1); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {}); } else { // Building - force ejection this.Eject(msg.entity, true); } } } }; /** * Update list of garrisoned entities if one gets renamed (e.g. by promotion) */ GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg) { var entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { this.entities[entityIndex] = msg.newentity; Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {}); } }; /** * Eject all foreign garrisoned entities which are no more allied */ GarrisonHolder.prototype.OnDiplomacyChanged = function() { for (var i = this.entities.length; i > 0; --i) { if (!IsOwnedByMutualAllyOfEntity(this.entity, this.entities[i-1])) this.Eject(this.entities[i-1], true); } this.UpdateGarrisonFlag(); }; Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 13693) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 13694) @@ -1,261 +1,300 @@ // (A serious implementation of this might want to use C++ instead of JS // for performance; this is just for fun.) -const shortFinal = 2.5; +const SHORT_FINAL = 2.5; function UnitMotionFlying() {} UnitMotionFlying.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + + "" + + "" + + "" + ""; UnitMotionFlying.prototype.Init = function() { this.hasTarget = false; this.reachedTarget = false; this.targetX = 0; this.targetZ = 0; this.targetMinRange = 0; this.targetMaxRange = 0; this.speed = 0; this.landing = false; this.onGround = true; + this.pitch = 0; + this.roll = 0; + this.waterDeath = false; }; UnitMotionFlying.prototype.OnUpdate = function(msg) { var turnLength = msg.turnLength; - if (!this.hasTarget) return; - + var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var pos = cmpPosition.GetPosition(); - var angle = cmpPosition.GetRotation().y; - + var angle = cmpPosition.GetRotation().y; + var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); + var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); + var ground = Math.max(cmpTerrain.GetGroundLevel(pos.x, pos.z), cmpWaterManager.GetWaterLevel(pos.x, pos.z)); + var newangle = angle; var canTurn = true; - - if (!this.landing) - { - // If we haven't reached max speed yet then we're still on the ground; - // otherwise we're taking off or flying - // this.onGround in case of a go-around after landing (but not fully stopped) - var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); - var ground = cmpTerrain.GetGroundLevel(pos.x, pos.z); - if (this.speed < this.template.TakeoffSpeed && this.onGround) - { - // Accelerate forwards - this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate); - canTurn = false; - // Clamp to ground if below it, or descend if above - if (pos.y < ground) - pos.y = ground; - else if (pos.y > ground) - pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate); - } - else - { - this.onGround = false; - // Climb/sink to max height above ground - this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate); - var targetHeight = ground + (+this.template.FlyingHeight); - if (pos.y < targetHeight) - pos.y = Math.min(targetHeight, pos.y + turnLength * this.template.ClimbRate); - else if (pos.y > targetHeight) - pos.y = Math.max(targetHeight, pos.y - turnLength * this.template.ClimbRate); - } - cmpPosition.SetHeightFixed(pos.y); - } - else + if (this.landing) { if (this.speed > 0 && this.onGround) - { + { + this.pitch = 0; // Deaccelerate forwards...at a very reduced pace. - this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate); + if (this.waterDeath) + this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate * 10); + else + this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate); canTurn = false; - // Clamp to ground if below it, or descend if above - var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); - var ground = cmpTerrain.GetGroundLevel(pos.x, pos.z); + // Clamp to ground if below it, or descend if above if (pos.y < ground) pos.y = ground; else if (pos.y > ground) - pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate); - cmpPosition.SetHeightFixed(pos.y); + pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate); } - else if (this.speed == 0) + else if (this.speed == 0 && this.onGround) { + if (this.waterDeath) + cmpHealth.Kill(); + this.pitch = 0; // We've stopped. + cmpGarrisonHolder.AllowGarrisoning(true,"UnitMotionFlying") canTurn = false; this.hasTarget = false; this.landing = false; } else { // Final Approach // We need to slow down to land! this.speed = Math.max(this.template.LandingSpeed, this.speed - turnLength * this.template.SlowingRate); - canTurn = false; - var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); - var ground = cmpTerrain.GetGroundLevel(pos.x, pos.z); + canTurn = false; var targetHeight = ground; // Steep, then gradual descent. - var descentRate = ((pos.y - targetHeight) / this.template.FlyingHeight * this.template.ClimbRate + shortFinal) * shortFinal; + if ((pos.y - targetHeight) / this.template.FlyingHeight > 1 / SHORT_FINAL) + this.pitch = - Math.PI / 18; + else + this.pitch = Math.PI / 18; + var descentRate = ((pos.y - targetHeight) / this.template.FlyingHeight * this.template.ClimbRate + SHORT_FINAL) * SHORT_FINAL; if (pos.y < targetHeight) pos.y = Math.max(targetHeight, pos.y + turnLength * descentRate); else if (pos.y > targetHeight) pos.y = Math.max(targetHeight, pos.y - turnLength * descentRate); if (targetHeight == pos.y) - this.onGround = true; - cmpPosition.SetHeightFixed(pos.y); + { + this.onGround = true; + if (targetHeight == cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater) + this.waterDeath = true; + } } } + else + { + // If we haven't reached max speed yet then we're still on the ground; + // otherwise we're taking off or flying + // this.onGround in case of a go-around after landing (but not fully stopped) + if (this.speed < this.template.TakeoffSpeed && this.onGround) + { + cmpGarrisonHolder.AllowGarrisoning(false,"UnitMotionFlying") + this.pitch = 0; + // Accelerate forwards + this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate); + canTurn = false; + // Clamp to ground if below it, or descend if above + if (pos.y < ground) + pos.y = ground; + else if (pos.y > ground) + pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate); + } + else + { + this.onGround = false; + // Climb/sink to max height above ground + this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate); + var targetHeight = ground + (+this.template.FlyingHeight); + if (Math.abs(pos.y-targetHeight) > this.template.FlyingHeight/5) + { + this.pitch = Math.PI / 9; + canTurn = false; + } + else + this.pitch = 0; + if (pos.y < targetHeight) + pos.y = Math.min(targetHeight, pos.y + turnLength * this.template.ClimbRate); + else if (pos.y > targetHeight) + { + pos.y = Math.max(targetHeight, pos.y - turnLength * this.template.ClimbRate); + this.pitch = -1 * this.pitch; + } + } + } + // If we're in range of the target then tell people that we've reached it // (TODO: quantisation breaks this) var distFromTarget = Math.sqrt(Math.pow(this.targetX - pos.x, 2) + Math.pow(this.targetZ - pos.z, 2)); if (!this.reachedTarget && this.targetMinRange <= distFromTarget && distFromTarget <= this.targetMaxRange) { this.reachedTarget = true; Engine.PostMessage(this.entity, MT_MotionChanged, { "starting": false, "error": false }); } // If we're facing away from the target, and are still fairly close to it, // then carry on going straight so we overshoot in a straight line var isBehindTarget = ((this.targetX - pos.x) * Math.sin(angle) + (this.targetZ - pos.z) * Math.cos(angle) < 0); // Overshoot the target: carry on straight if (isBehindTarget && distFromTarget < this.template.MaxSpeed * this.template.OvershootTime) canTurn = false; if (canTurn) { // Turn towards the target var targetAngle = Math.atan2(this.targetX - pos.x, this.targetZ - pos.z); var delta = targetAngle - angle; // Wrap delta to -pi..pi delta = (delta + Math.PI) % (2*Math.PI); // range -2pi..2pi if (delta < 0) delta += 2*Math.PI; // range 0..2pi delta -= Math.PI; // range -pi..pi // Clamp to max rate var deltaClamped = Math.min(Math.max(delta, -this.template.TurnRate * turnLength), this.template.TurnRate * turnLength); // Calculate new orientation, in a peculiar way in order to make sure the // result gets close to targetAngle (rather than being n*2*pi out) - angle = targetAngle + deltaClamped - delta; + newangle = targetAngle + deltaClamped - delta; + if (newangle - angle > Math.PI / 18) + this.roll = Math.PI / 9; + else if (newangle - angle < -Math.PI / 18) + this.roll = - Math.PI / 9; + else + this.roll = newangle - angle; } + else + this.roll = 0; pos.x += this.speed * turnLength * Math.sin(angle); pos.z += this.speed * turnLength * Math.cos(angle); - - cmpPosition.TurnTo(angle); + cmpPosition.SetHeightFixed(pos.y); + cmpPosition.TurnTo(newangle); + cmpPosition.SetXZRotation(this.pitch, this.roll); cmpPosition.MoveTo(pos.x, pos.z); }; UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange) { this.hasTarget = true; this.reachedTarget = false; this.targetX = x; this.targetZ = z; this.targetMinRange = minRange; this.targetMaxRange = maxRange; return true; }; UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange) { var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return false; var targetPos = cmpTargetPosition.GetPosition2D(); this.hasTarget = true; this.reachedTarget = false; this.targetX = targetPos.x; this.targetZ = targetPos.y; this.targetMinRange = minRange; this.targetMaxRange = maxRange; return true; }; UnitMotionFlying.prototype.IsInPointRange = function(x, y, minRange, maxRange) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var pos = cmpPosition.GetPosition2D(); var distFromTarget = Math.sqrt(Math.pow(x - pos.x, 2) + Math.pow(y - pos.y, 2)); if (minRange <= distFromTarget && distFromTarget <= maxRange) return true; return false; }; UnitMotionFlying.prototype.IsInTargetRange = function(target, minRange, maxRange) { var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return false; var targetPos = cmpTargetPosition.GetPosition2D(); return this.IsInPointRange(targetPos.x, targetPos.y, minRange, maxRange); }; UnitMotionFlying.prototype.GetWalkSpeed = function() { return +this.template.MaxSpeed; }; UnitMotionFlying.prototype.GetRunSpeed = function() { return this.GetWalkSpeed(); }; UnitMotionFlying.prototype.GetCurrentSpeed = function() { return this.speed; } UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z) { // Ignore this - angle is controlled by the target-seeking code instead }; UnitMotionFlying.prototype.StopMoving = function() { //Invert - this.landing = !this.landing; + if (!this.waterDeath) + this.landing = !this.landing; + }; UnitMotionFlying.prototype.SetDebugOverlay = function(enabled) { }; Engine.RegisterComponentType(IID_UnitMotion, "UnitMotionFlying", UnitMotionFlying); Index: ps/trunk/binaries/data/mods/public/simulation/templates/other/plane.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/other/plane.xml (revision 13693) +++ ps/trunk/binaries/data/mods/public/simulation/templates/other/plane.xml (revision 13694) @@ -1,47 +1,74 @@ 0.0 100.0 25.0 48 24 60.0 0 2000 1.5 + + 3 + 1 + Infantry + + + 1 + 0 + Support Infantry + 1 + 5 + false + + + + + 0.0 + 3.0 + 7.0 + hele P-51 Mustang This may be anachronistic. A World War 2 American fighter plane. units/global_mustang.png + + 100 + true + true + 1.0 + true 60.0 50.0 40.0 25.0 5.0 10.0 1.0 2.0 50.0 - 5.0 + 15.0 + true 100 units/global/plane.xml