Index: binaries/data/mods/public/simulation/components/UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitMotionFlying.js +++ /dev/null @@ -1,341 +0,0 @@ -// (A serious implementation of this might want to use C++ instead of JS -// for performance; this is just for fun.) -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; - this.passabilityClass = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).GetPassabilityClass(this.template.PassabilityClass); -}; - -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 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 (this.speed > 0 && this.onGround) - { - if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater == "true") - this.waterDeath = true; - this.pitch = 0; - // Deaccelerate forwards...at a very reduced pace. - 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 - if (pos.y < ground) - pos.y = ground; - else if (pos.y > ground) - pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate); - } - else if (this.speed == 0 && this.onGround) - { - if (this.waterDeath && cmpHealth) - cmpHealth.Kill(); - else - { - this.pitch = 0; - // We've stopped. - if (cmpGarrisonHolder) - cmpGarrisonHolder.AllowGarrisoning(true,"UnitMotionFlying"); - canTurn = false; - this.hasTarget = false; - this.landing = false; - // summon planes back from the edge of the map - var terrainSize = cmpTerrain.GetMapSize(); - var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - if (cmpRangeManager.GetLosCircular()) - { - var mapRadius = terrainSize/2; - var x = pos.x - mapRadius; - var z = pos.z - mapRadius; - var div = (mapRadius - 12) / Math.sqrt(x*x + z*z); - if (div < 1) - { - pos.x = mapRadius + x*div; - pos.z = mapRadius + z*div; - newangle += Math.PI; - } - } - else - { - pos.x = Math.max(Math.min(pos.x, terrainSize - 12), 12); - pos.z = Math.max(Math.min(pos.z, terrainSize - 12), 12); - newangle += Math.PI; - } - } - } - 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 targetHeight = ground; - // Steep, then gradual descent. - 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; - 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) - { - if (cmpGarrisonHolder) - 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.euclidDistance2D(pos.x, pos.z, this.targetX, this.targetZ); - if (!this.reachedTarget && this.targetMinRange <= distFromTarget && distFromTarget <= this.targetMaxRange) - { - this.reachedTarget = true; - Engine.PostMessage(this.entity, MT_MotionUpdate, { "updateString": "likelySuccess" }); - } - - // 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) - 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.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.landing = false; - 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.GetWalkSpeed = function() -{ - return +this.template.MaxSpeed; -}; - -UnitMotionFlying.prototype.SetSpeedMultiplier = function() -{ - // ignore this, the speed is always the walk speed -}; - -UnitMotionFlying.prototype.GetRunMultiplier = function() -{ - return 1; -}; - -UnitMotionFlying.prototype.IsMoveRequested = function() -{ - return this.hasTarget; -}; - -UnitMotionFlying.prototype.GetCurrentSpeed = function() -{ - return this.speed; -}; - -UnitMotionFlying.prototype.GetSpeedMultiplier = function() -{ - return this.GetCurrentSpeed() / this.GetWalkSpeed(); -}; - -UnitMotionFlying.prototype.GetPassabilityClassName = function() -{ - return this.template.PassabilityClass; -}; - -UnitMotionFlying.prototype.GetPassabilityClass = function() -{ - return this.passabilityClass; -}; - -UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z) -{ - // Ignore this - angle is controlled by the target-seeking code instead -}; - -UnitMotionFlying.prototype.SetFacePointAfterMove = function() -{ - // Ignore this - angle is controlled by the target-seeking code instead -}; - -UnitMotionFlying.prototype.StopMoving = function() -{ - //Invert - if (!this.waterDeath) - this.landing = !this.landing; - -}; - -UnitMotionFlying.prototype.SetDebugOverlay = function(enabled) -{ -}; - -Engine.RegisterComponentType(IID_UnitMotion, "UnitMotionFlying", UnitMotionFlying); Index: binaries/data/mods/public/simulation/components/interfaces/GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/interfaces/GarrisonHolder.js @@ -1,5 +1,3 @@ -Engine.RegisterInterface("GarrisonHolder"); - /** * Message of the form { "added": number[], "removed": number[] } * sent from the GarrisonHolder component to the current entity whenever the garrisoned units change. Index: binaries/data/mods/public/simulation/components/interfaces/Health.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/Health.js +++ binaries/data/mods/public/simulation/components/interfaces/Health.js @@ -1,5 +1,3 @@ -Engine.RegisterInterface("Health"); - /** * Message of the form { "from": number, "to": number } * sent from Health component whenever health changes. Index: binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js @@ -1,9 +1,11 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); +Engine.RegisterInterface("GarrisonHolder"); // Declaration is in C++. Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Auras.js"); +Engine.RegisterInterface("Health"); // Declaration is in C++. Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Health.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Health.js +++ binaries/data/mods/public/simulation/components/tests/test_Health.js @@ -4,7 +4,7 @@ Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); - +Engine.RegisterInterface("Health"); // Declaration is in C++. Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("Health.js"); Index: binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js +++ /dev/null @@ -1,143 +0,0 @@ -Engine.LoadComponentScript("UnitMotionFlying.js"); -Engine.LoadComponentScript("interfaces/Health.js"); -Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); - -let entity = 1; -let target = 2; - -let height = 5; - -AddMock(SYSTEM_ENTITY, IID_Pathfinder, { - GetPassabilityClass: (name) => 1 << 8 -}); - -let cmpUnitMotionFlying = ConstructComponent(entity, "UnitMotionFlying", { - "MaxSpeed": 1.0, - "TakeoffSpeed": 0.5, - "LandingSpeed": 0.5, - "AccelRate": 0.0005, - "SlowingRate": 0.001, - "BrakingRate": 0.0005, - "TurnRate": 0.1, - "OvershootTime": 10, - "FlyingHeight": 100, - "ClimbRate": 0.1, - "DiesInWater": false, - "PassabilityClass": "unrestricted" -}); - -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 0); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetRunMultiplier(), 1); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); -cmpUnitMotionFlying.SetSpeedMultiplier(2); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 0); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetRunMultiplier(), 1); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); - -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClassName(), "unrestricted"); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClass(), 1 << 8); - -AddMock(entity, IID_Position, { - "IsInWorld": () => true, - "GetPosition2D": () => { return { "x": 50, "y": 100 }; }, - "GetPosition": () => { return { "x": 50, "y": height, "z": 100 }; }, - "GetRotation": () => { return { "y": 3.14 }; }, - "SetHeightFixed": (y) => height = y, - "TurnTo": () => {}, - "SetXZRotation": () => {}, - "MoveTo": () => {} -}); - -AddMock(target, IID_Position, { - "IsInWorld": () => true, - "GetPosition2D": () => { return { "x": 100, "y": 200 }; } -}); - -AddMock(entity, IID_GarrisonHolder, { - "AllowGarrisoning": () => {} -}); - -AddMock(entity, IID_Health, { -}); - -AddMock(entity, IID_RangeManager, { - "GetLosCircular": () => true -}); - -AddMock(entity, IID_Terrain, { - "GetGroundLevel": () => 4, - "GetMapSize": () => 20 -}); - -AddMock(entity, IID_WaterManager, { - "GetWaterLevel": () => 5 -}); - -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 0); - -TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToTargetRange(target, 0, 10), true); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToPointRange(100, 200, 0, 20), true); - -// Take Off -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25); -TS_ASSERT_EQUALS(height, 5); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5); -TS_ASSERT_EQUALS(height, 5); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5); -TS_ASSERT_EQUALS(height, 5); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.75); -TS_ASSERT_EQUALS(height, 55); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 1); -TS_ASSERT_EQUALS(height, 105); - -// Fly -cmpUnitMotionFlying.OnUpdate({ "turnLength": 100 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); -TS_ASSERT_EQUALS(height, 105); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); -TS_ASSERT_EQUALS(height, 105); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); -TS_ASSERT_EQUALS(height, 105); - -// Land -cmpUnitMotionFlying.StopMoving(); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); -TS_ASSERT_EQUALS(height, 105); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5); -TS_ASSERT_EQUALS(height, 5); - -// Slide -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25); -TS_ASSERT_EQUALS(height, 5); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25); -TS_ASSERT_EQUALS(height, 5); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); -TS_ASSERT_EQUALS(height, 5); - -// Stay -cmpUnitMotionFlying.OnUpdate({ "turnLength": 300 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); -TS_ASSERT_EQUALS(height, 5); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); -TS_ASSERT_EQUALS(height, 5); -cmpUnitMotionFlying.OnUpdate({ "turnLength": 900 }); -TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); -TS_ASSERT_EQUALS(height, 5); - Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -98,9 +98,15 @@ INTERFACE(Footprint) COMPONENT(Footprint) +INTERFACE(GarrisonHolder) +COMPONENT(GarrisonHolderScripted) + INTERFACE(GuiInterface) COMPONENT(GuiInterfaceScripted) +INTERFACE(Health) +COMPONENT(HealthScripted) + INTERFACE(Identity) COMPONENT(IdentityScripted) @@ -187,6 +193,8 @@ COMPONENT(UnitMotion) // must be after Obstruction COMPONENT(UnitMotionScripted) +COMPONENT(UnitMotionFlying) // must be after Obstruction + INTERFACE(UnitRenderer) COMPONENT(UnitRenderer) Index: source/simulation2/components/CCmpUnitMotionFlying.h =================================================================== --- /dev/null +++ source/simulation2/components/CCmpUnitMotionFlying.h @@ -0,0 +1,95 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_CCMPUNITMOTIONFLYING +#define INCLUDED_CCMPUNITMOTIONFLYING + +#include "ICmpUnitMotion.h" +#include "simulation2/system/Component.h" +#include "simulation2/components/ICmpGarrisonHolder.h" +#include "simulation2/components/ICmpHealth.h" +#include "simulation2/components/ICmpPosition.h" +#include "simulation2/components/ICmpTerrain.h" +#include "simulation2/components/ICmpWaterManager.h" +#include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/MessageTypes.h" +#include "ps/CLogger.h" + +class CCmpUnitMotionFlying : public ICmpUnitMotion +{ +public: + DEFAULT_COMPONENT_ALLOCATOR(UnitMotionFlying) + static void ClassInit(CComponentManager& componentManager); + static std::string GetSchema(); + virtual void Serialize(ISerializer& serializer); + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserializer); + virtual void Init(const CParamNode& paramNode); + virtual void Deinit(); + virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)); + void Move(fixed turnLength); + virtual void StopMoving(); + virtual void SetSpeedMultiplier(fixed UNUSED(multiplier)); + virtual void FaceTowardsPoint(fixed x, fixed z); + virtual void SetDebugOverlay(bool enabled); + virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); + virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z); + virtual pass_class_t GetPassabilityClass() const; + virtual std::string GetPassabilityClassName() const; + virtual fixed GetSpeedMultiplier() const; + virtual fixed GetCurrentSpeed() const; + virtual fixed GetSpeed() const; + virtual fixed GetRunMultiplier() const; + virtual fixed GetWalkSpeed() const; + virtual bool IsMoveRequested() const; + virtual entity_pos_t GetUnitClearance() const; + virtual void SetFacePointAfterMove(bool facePointAfterMove); + +private: + fixed m_TargetX; + fixed m_TargetZ; + fixed m_TargetMinRange; + fixed m_StationaryDistance; + fixed m_TargetMaxRange; + fixed m_Speed; + fixed m_MaxSpeed; + fixed m_TurnRate; + fixed m_BrakingRate; + fixed m_ClimbRate; + fixed m_FlyingHeight; + fixed m_LandingSpeed; + fixed m_SlowingRate; + fixed m_OvershootTime; + fixed m_TakeOffSpeed; + fixed m_AccelRate; + fixed m_Pitch; + fixed m_Roll; + bool m_HasTarget; + bool m_ReachedTarget; + bool m_DiesInWater; + bool m_Landing; + bool m_OnGround; + bool m_WaterDeath; + std::string m_PassabilityClassName; + pass_class_t m_PassabilityClass; + fixed EuclidDistance(fixed x1, fixed z1, fixed x2, fixed z2); +}; + + +REGISTER_COMPONENT_TYPE(UnitMotionFlying) + +#endif // INCLUDED_CCMPUNITMOTIONFLYING Index: source/simulation2/components/CCmpUnitMotionFlying.cpp =================================================================== --- /dev/null +++ source/simulation2/components/CCmpUnitMotionFlying.cpp @@ -0,0 +1,451 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "CCmpUnitMotionFlying.h" + +constexpr float SHORT_FINAL = 2.5f; +constexpr double M_PI_NINTH = M_PI / 9.0; +constexpr double M_TWO_PI = M_PI * 2.0; +constexpr double M_PI_EIGHTEENTH = M_PI / 18.0; + +std::string CCmpUnitMotionFlying::GetSchema() +{ + return + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; +} + +void CCmpUnitMotionFlying::Serialize(ISerializer& serializer) +{ +} + +void CCmpUnitMotionFlying::Deserialize(const CParamNode& paramNode, IDeserializer& deserializer) +{ +} + + +void CCmpUnitMotionFlying::ClassInit(CComponentManager& componentManager) +{ + componentManager.SubscribeToMessageType(MT_Update); +} + +void CCmpUnitMotionFlying::Init(const CParamNode ¶mNode) +{ + m_HasTarget = false; + m_ReachedTarget = false; + m_TargetX = fixed::Zero(); + m_TargetZ = fixed::Zero(); + m_TargetMinRange = fixed::Zero(); + m_TargetMaxRange = fixed::Zero(); + m_Speed = fixed::Zero(); + m_Landing = false; + m_OnGround = true; + m_Pitch = fixed::Zero(); + m_Roll = fixed::Zero(); + m_WaterDeath = false; + m_BrakingRate = paramNode.GetChild("BrakingRate").ToFixed(); + m_ClimbRate = paramNode.GetChild("ClimbRate").ToFixed(); + m_TurnRate = paramNode.GetChild("TurnRate").ToFixed(); + m_MaxSpeed = paramNode.GetChild("MaxSpeed").ToFixed(); + m_DiesInWater = paramNode.GetChild("MaxSpeed").ToBool(); + m_FlyingHeight =paramNode.GetChild("FlyingHeight").ToFixed(); + m_LandingSpeed = paramNode.GetChild("LandingSpeed").ToFixed(); + m_SlowingRate = paramNode.GetChild("SlowingRate").ToFixed(); + m_OvershootTime = paramNode.GetChild("OvershootTime").ToFixed(); + m_TakeOffSpeed = paramNode.GetChild("TakeoffSpeed").ToFixed(); + m_AccelRate = paramNode.GetChild("AccelRate").ToFixed(); + m_PassabilityClassName = paramNode.GetChild("PassabilityClass").ToUTF8(); + + if (paramNode.GetChild("StationaryDistance").IsOk()) + m_StationaryDistance = paramNode.GetChild("StationaryDistance").ToFixed(); + else + m_StationaryDistance = fixed::FromInt(-1); + + CmpPtr cmpPathfinder(GetSystemEntity()); + if (cmpPathfinder) + m_PassabilityClass = cmpPathfinder->GetPassabilityClass(m_PassabilityClassName); +} + +bool CCmpUnitMotionFlying::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) +{ + m_HasTarget = true; + m_Landing = false; + m_ReachedTarget = false; + m_TargetX = x; + m_TargetZ = z; + m_TargetMinRange = minRange; + m_TargetMaxRange = maxRange; + + return true; +}; + +bool CCmpUnitMotionFlying::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +{ + CmpPtr cmpTargetPosition(GetEntityHandle()); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) + return false; + + CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); + return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange); +} + +fixed CCmpUnitMotionFlying::GetWalkSpeed() const +{ + return m_MaxSpeed; +} + +void CCmpUnitMotionFlying::SetSpeedMultiplier(fixed UNUSED(multiplier)) +{ + // Ignore this, the speed is always the walk speed. +} + +fixed CCmpUnitMotionFlying::GetRunMultiplier() const +{ + return fixed::FromInt(1); +} + +bool CCmpUnitMotionFlying::IsMoveRequested() const +{ + return m_HasTarget; +} + +entity_pos_t CCmpUnitMotionFlying::GetUnitClearance() const +{ + return entity_pos_t::Zero(); +} + + +void CCmpUnitMotionFlying::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) +{ +} + +fixed CCmpUnitMotionFlying::GetCurrentSpeed() const +{ + return m_Speed; +} + +fixed CCmpUnitMotionFlying::GetSpeed() const +{ + return GetCurrentSpeed(); +} + +fixed CCmpUnitMotionFlying::GetSpeedMultiplier() const +{ + return m_Speed / m_MaxSpeed; +} + +std::string CCmpUnitMotionFlying::GetPassabilityClassName() const +{ + return m_PassabilityClassName; +} + +pass_class_t CCmpUnitMotionFlying::GetPassabilityClass() const +{ + return m_PassabilityClass; +} + +void CCmpUnitMotionFlying::FaceTowardsPoint(fixed x, fixed z) +{ + // Ignore this - angle is controlled by the target-seeking code instead +} + +void CCmpUnitMotionFlying::SetFacePointAfterMove(bool facePointAfterMove) +{ + // Ignore this - angle is controlled by the target-seeking code instead +} + +void CCmpUnitMotionFlying::StopMoving() +{ + // Invert. + if (!m_WaterDeath) + m_Landing = !m_Landing; +} + +void CCmpUnitMotionFlying::SetDebugOverlay(bool enabled) +{ + // TODO: Implement. +} + +void CCmpUnitMotionFlying::Deinit() +{ +} + +void CCmpUnitMotionFlying::Move(fixed turnLength) +{ + if (!m_HasTarget) + return; + + CmpPtr cmpTerrain(GetSystemEntity()); + if (!cmpTerrain) + return; + + CmpPtr cmpWaterManager(GetSystemEntity()); + if (!cmpWaterManager) + return; + + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + fixed sinus = fixed::Zero(); + fixed cosinus = fixed::Zero(); + CFixedVector3D pos = cmpPosition->GetPosition(); + fixed ground = std::max(cmpTerrain->GetGroundLevel(pos.X, pos.Z), cmpWaterManager->GetWaterLevel(pos.X, pos.Z)); + fixed angle = cmpPosition->GetRotation().Y; + fixed newangle = angle; + bool canTurn = true; + if (m_Landing) + { + if (m_Speed > fixed::Zero() && m_OnGround) + { + if (pos.Y <= cmpWaterManager->GetWaterLevel(pos.X, pos.Z) && m_DiesInWater) + m_WaterDeath = true; + m_Pitch = fixed::Zero(); + // Deaccelerate forwards...at a very reduced pace. + if (m_WaterDeath) + m_Speed = std::max(fixed::Zero(), m_Speed - turnLength.Multiply(m_BrakingRate.Multiply(fixed::FromInt(10)))); + else + m_Speed = std::max(fixed::Zero(), m_Speed - turnLength.Multiply(m_BrakingRate)); + canTurn = false; + + // Clamp to ground if below it, or descend if above + pos.Y = pos.Y < ground ? ground : std::max(ground, pos.Y - turnLength.Multiply(m_ClimbRate)); + } + else if (m_Speed.IsZero() && m_OnGround) + { + CmpPtr cmpHealth(GetEntityHandle()); + if (m_WaterDeath && cmpHealth) + cmpHealth->Kill(); + else + { + m_Pitch = fixed::Zero(); + CmpPtr cmpGarrisonHolder(GetEntityHandle()); + if (cmpGarrisonHolder) + cmpGarrisonHolder->AllowGarrisoning(true, "UnitMotionFlying"); + canTurn = false; + m_HasTarget = false; + m_Landing = false; + // summon planes back from the edge of the map + u32 terrainSize = cmpTerrain->GetMapSize(); + + CmpPtr cmpRangeManager(GetSystemEntity()); + if (cmpRangeManager->GetLosCircular()) + { + fixed mapRadius = fixed::FromDouble(terrainSize / 2.0); + fixed x = pos.X - mapRadius; + fixed z = pos.Z - mapRadius; + fixed div = (mapRadius - fixed::FromInt(12)) / (x.Square() + z.Square()).Sqrt(); + if (div < fixed::FromInt(1)) + { + pos.X = mapRadius + x.Multiply(div); + pos.Z = mapRadius + z.Multiply(div); + newangle += fixed::Pi(); + } + } + else + { + pos.X = std::max(std::min(pos.X, fixed::FromInt(terrainSize - 12)), fixed::FromInt(12)); + pos.Z = std::max(std::min(pos.Z, fixed::FromInt(terrainSize - 12)), fixed::FromInt(12)); + newangle += fixed::Pi(); + } + } + } + else + { + // Final Approach + // We need to slow down to land! + m_Speed = std::max(m_LandingSpeed, m_Speed - turnLength.Multiply(m_SlowingRate)); + canTurn = false; + fixed targetHeight = ground; + auto heightRatio = (pos.Y - targetHeight) / m_FlyingHeight; + // Steep, then gradual descent. + if (heightRatio > fixed::FromInt(1) / fixed::FromDouble(SHORT_FINAL)) + m_Pitch = - fixed::FromDouble(M_PI_EIGHTEENTH); + else + m_Pitch = fixed::FromDouble(M_PI_EIGHTEENTH); + fixed descentRate = (heightRatio.Multiply(m_ClimbRate) + fixed::FromDouble(SHORT_FINAL)).Multiply(fixed::FromDouble(SHORT_FINAL)); + if (pos.Y < targetHeight) + pos.Y = std::max(targetHeight, pos.Y + turnLength.Multiply(descentRate)); + else if (pos.Y > targetHeight) + pos.Y = std::max(targetHeight, pos.Y - turnLength.Multiply(descentRate)); + if (targetHeight == pos.Y) + { + m_OnGround = true; + if (targetHeight == cmpWaterManager->GetWaterLevel(pos.X, pos.Z) && m_DiesInWater) + m_WaterDeath = true; + } + } + } + else + { + if (m_StationaryDistance > fixed::Zero() && EuclidDistance(pos.X, pos.Z, m_TargetX, m_TargetZ) <= m_StationaryDistance.Square()) + return; + + // If we haven't reached max speed yet then we're still on the ground; + // otherwise we're taking off or flying. + // m_OnGround in case of a go-around after landing (but not fully stopped). + if (m_Speed < m_TakeOffSpeed && m_OnGround) + { + CmpPtr cmpGarrisonHolder(GetEntityHandle()); + if (cmpGarrisonHolder) + cmpGarrisonHolder->AllowGarrisoning(false, "UnitMotionFlying"); + m_Pitch = fixed::Zero(); + // Accelerate forwards + m_Speed = std::min(m_MaxSpeed, m_Speed + turnLength.Multiply(m_AccelRate)); + canTurn = false; + + // Clamp to ground if below it, or descend if above + pos.Y = pos.Y < ground ? ground : std::max(ground, pos.Y - turnLength.Multiply(m_ClimbRate)); + } + else + { + m_OnGround = false; + // Climb/sink to max height above ground + m_Speed = std::min(m_MaxSpeed, m_Speed + turnLength.Multiply(m_AccelRate)); + fixed targetHeight = ground + m_FlyingHeight; + if ((pos.Y - targetHeight).Absolute() > m_FlyingHeight / fixed::FromInt(5)) + { + m_Pitch = fixed::FromDouble(M_PI_NINTH); + canTurn = false; + } + else + m_Pitch = fixed::Zero(); + if (pos.Y < targetHeight) + pos.Y = std::min(targetHeight, pos.Y + turnLength.Multiply(m_ClimbRate)); + else if (pos.Y > targetHeight) + { + pos.Y = std::max(targetHeight, pos.Y - turnLength.Multiply(m_ClimbRate)); + m_Pitch = fixed::FromInt(-1).Multiply(m_Pitch); + } + } + } + + // If we're in range of the target then tell people that we've reached it + // (TODO: quantisation breaks this) + fixed distFromTarget = EuclidDistance(pos.X, pos.Z, m_TargetX, m_TargetZ); + if (!m_ReachedTarget && m_TargetMinRange.Square() <= distFromTarget && distFromTarget <= m_TargetMaxRange.Square()) + { + m_ReachedTarget = true; + CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_SUCCESS); + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } + + + + sincos_approx(angle, sinus, cosinus); + // 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 + bool isBehindTarget = ((m_TargetX - pos.X).Multiply(sinus) + (m_TargetZ - pos.Z).Multiply(cosinus) < fixed::Zero()); + // Overshoot the target: carry on straight + if (isBehindTarget && distFromTarget < m_MaxSpeed.Multiply(m_OvershootTime).Square()) + canTurn = false; + + if (canTurn) + { + // Turn towards the target + fixed targetAngle = atan2_approx(m_TargetX - pos.X, m_TargetZ - pos.Z); + fixed delta = targetAngle - angle; + // Wrap delta to -pi..pi + delta = (delta + fixed::Pi()) % fixed::FromDouble(M_TWO_PI); // range -2pi..2pi + if (delta < fixed::Zero()) + delta += fixed::FromDouble(M_TWO_PI); // range 0..2pi + delta -= fixed::Pi(); // range -pi..pi + // Clamp to max rate + fixed deltaClamped = std::min(std::max(delta, - m_TurnRate.Multiply(turnLength)), m_TurnRate.Multiply(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) + newangle = targetAngle + deltaClamped - delta; + if (newangle - angle > fixed::FromDouble(M_PI_EIGHTEENTH)) + m_Roll = fixed::FromDouble(M_PI_NINTH); + else if (newangle - angle < - fixed::FromDouble(M_PI_EIGHTEENTH)) + m_Roll = -fixed::FromDouble(M_PI_NINTH); + else + m_Roll = newangle - angle; + } + else + m_Roll = fixed::Zero(); + + sincos_approx(angle, sinus, cosinus); + fixed temp = m_Speed.Multiply(turnLength); + pos.X += temp.Multiply(sinus); + pos.Z += temp.Multiply(cosinus); + cmpPosition->SetHeightFixed(pos.Y); + cmpPosition->TurnTo(newangle); + cmpPosition->SetXZRotation(m_Pitch, m_Roll); + cmpPosition->MoveTo(pos.X, pos.Z); +} + +fixed CCmpUnitMotionFlying::EuclidDistance(fixed x1, fixed z1, fixed x2, fixed z2) +{ + int dx = (x1 - x2).ToInt_RoundToNearest(); + int dz = (z1 - z2).ToInt_RoundToNearest(); + return fixed::FromInt(dx + dz); +} + + +void CCmpUnitMotionFlying::HandleMessage(const CMessage& msg, bool UNUSED(global)) +{ + switch (msg.GetType()) + { + case MT_Update: + { + Move(static_cast (msg).turnLength); + break; + } + } +} Index: source/simulation2/components/ICmpGarrisonHolder.h =================================================================== --- /dev/null +++ source/simulation2/components/ICmpGarrisonHolder.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_ICMPGARRISONHOLDER +#define INCLUDED_ICMPGARRISONHOLDER + +#include "simulation2/system/Interface.h" + +class ICmpGarrisonHolder : public IComponent +{ +public: + virtual void AllowGarrisoning(bool allow, std::string callerId) = 0; + DECLARE_INTERFACE_TYPE(GarrisonHolder) +}; + +#endif // INCLUDED_ICMPGARRISONHOLDER Index: source/simulation2/components/ICmpGarrisonHolder.cpp =================================================================== --- /dev/null +++ source/simulation2/components/ICmpGarrisonHolder.cpp @@ -0,0 +1,40 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ICmpGarrisonHolder.h" + +#include "simulation2/system/InterfaceScripted.h" +#include "simulation2/scripting/ScriptComponent.h" + + +BEGIN_INTERFACE_WRAPPER(GarrisonHolder) +END_INTERFACE_WRAPPER(GarrisonHolder) + +class CCmpGarrisonHolderScripted : public ICmpGarrisonHolder +{ +public: + DEFAULT_SCRIPT_WRAPPER(GarrisonHolderScripted) + + virtual void AllowGarrisoning(bool allow, std::string callerId) + { + m_Script.CallVoid("AllowGarrisoning", allow, callerId); + } +}; + +REGISTER_COMPONENT_SCRIPT_WRAPPER(GarrisonHolderScripted) Index: source/simulation2/components/ICmpHealth.h =================================================================== --- /dev/null +++ source/simulation2/components/ICmpHealth.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_ICMPHEALTH +#define INCLUDED_ICMPHEALTH + +#include "simulation2/system/Interface.h" + +class ICmpHealth : public IComponent +{ +public: + virtual void Kill() = 0; + DECLARE_INTERFACE_TYPE(Health) +}; + +#endif // INCLUDED_ICMPHEALTH Index: source/simulation2/components/ICmpHealth.cpp =================================================================== --- /dev/null +++ source/simulation2/components/ICmpHealth.cpp @@ -0,0 +1,40 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ICmpHealth.h" + +#include "simulation2/system/InterfaceScripted.h" +#include "simulation2/scripting/ScriptComponent.h" + + +BEGIN_INTERFACE_WRAPPER(Health) +END_INTERFACE_WRAPPER(Health) + +class CCmpHealthScripted: public ICmpHealth +{ +public: + DEFAULT_SCRIPT_WRAPPER(HealthScripted) + + virtual void Kill() + { + m_Script.CallVoid("Kill"); + } +}; + +REGISTER_COMPONENT_SCRIPT_WRAPPER(HealthScripted)