diff --git a/binaries/data/mods/public/gui/session_new/input.js b/binaries/data/mods/public/gui/session_new/input.js index 6d62b3108b..c08ee90c03 100644 --- a/binaries/data/mods/public/gui/session_new/input.js +++ b/binaries/data/mods/public/gui/session_new/input.js @@ -21,9 +21,9 @@ function updateCursor() var action = determineAction(mouseX, mouseY); if (action) { - if (action.type != "move") + if (action.cursor) { - Engine.SetCursor("action-" + action.type); + Engine.SetCursor(action.cursor); return; } } @@ -31,6 +31,16 @@ function updateCursor() Engine.SetCursor("arrow-default"); } +function findGatherType(gatherer, supply) +{ + if (!gatherer || !supply) + return; + if (gatherer[supply.type.generic+"."+supply.type.specific]) + return supply.type.specific; + if (gatherer[supply.type.generic]) + return supply.type.generic; +} + /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ @@ -61,7 +71,11 @@ function determineAction(x, y) // Different owner -> attack if (entState.attack && targetState.player != player) - return {"type": "attack", "target": targets[0]}; + return {"type": "attack", "cursor": "action-attack", "target": targets[0]}; + + var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply); + if (resource) + return {"type": "gather", "cursor": "action-gather-"+resource, "target": targets[0]}; // TODO: need more actions @@ -140,6 +154,10 @@ function handleInputAfterGui(ev) case "attack": Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target}); return true; + + case "gather": + Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target}); + return true; } } } diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index a391045baf..2adf2e443b 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -28,37 +28,13 @@ Attack.prototype.GetAttackStrengths = function() }; }; -function hypot2(x, y) +Attack.prototype.GetRange = function() { - return x*x + y*y; -} - -Attack.prototype.CheckRange = function(target) -{ - // Target must be in the world - var cmpPositionTarget = Engine.QueryInterface(target, IID_Position); - if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) - return { "error": "not-in-world" }; - - // We must be in the world - var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld()) - return { "error": "not-in-world" }; - - // Target must be within range - var posTarget = cmpPositionTarget.GetPosition(); - var posSelf = cmpPositionSelf.GetPosition(); - var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z); - // TODO: ought to be distance to closest point in footprint, not to center - var maxrange = +this.template.Range; - if (dist2 > maxrange*maxrange) - return { "error": "out-of-range", "maxrange": maxrange }; - - return {}; + return { "max": +this.template.Range, "min": 0 }; } /** - * Attack the target entity. This should only be called after a successful CheckRange, + * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ diff --git a/binaries/data/mods/public/simulation/components/Cost.js b/binaries/data/mods/public/simulation/components/Cost.js index a1d955759c..3cfb0a20c0 100644 --- a/binaries/data/mods/public/simulation/components/Cost.js +++ b/binaries/data/mods/public/simulation/components/Cost.js @@ -6,16 +6,26 @@ Cost.prototype.Init = function() Cost.prototype.GetPopCost = function() { - if ('Population' in this.template) + if ("Population" in this.template) return +this.template.Population; return 0; }; Cost.prototype.GetPopBonus = function() { - if ('PopulationBonus' in this.template) + if ("PopulationBonus" in this.template) return +this.template.PopulationBonus; return 0; }; +Cost.prototype.GetResourceCosts = function() +{ + return { + "food": +(this.template.Resources.food || 0), + "wood": +(this.template.Resources.wood || 0), + "stone": +(this.template.Resources.stone || 0), + "metal": +(this.template.Resources.metal || 0) + }; +}; + Engine.RegisterComponentType(IID_Cost, "Cost", Cost); diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index fc040b85c6..e524262d58 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -20,7 +20,8 @@ GuiInterface.prototype.GetSimulationState = function(player) var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); var player = { "popCount": cmpPlayer.GetPopulationCount(), - "popLimit": cmpPlayer.GetPopulationLimit() + "popLimit": cmpPlayer.GetPopulationLimit(), + "resourceCounts": cmpPlayer.GetResourceCounts() }; ret.players.push(player); } @@ -62,6 +63,22 @@ GuiInterface.prototype.GetEntityState = function(player, ent) ret.player = cmpOwnership.GetOwner(); } + var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); + if (cmpResourceSupply) + { + ret.resourceSupply = { + "max": cmpResourceSupply.GetMaxAmount(), + "amount": cmpResourceSupply.GetCurrentAmount(), + "type": cmpResourceSupply.GetType() + }; + } + + var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); + if (cmpResourceGatherer) + { + ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); + } + return ret; }; diff --git a/binaries/data/mods/public/simulation/components/Player.js b/binaries/data/mods/public/simulation/components/Player.js index ce726666d3..1638256501 100644 --- a/binaries/data/mods/public/simulation/components/Player.js +++ b/binaries/data/mods/public/simulation/components/Player.js @@ -7,6 +7,12 @@ Player.prototype.Init = function() this.civ = "celt"; this.popCount = 0; this.popLimit = 50; + this.resourceCount = { + "food": 100, + "wood": 50, + "metal": 0, + "stone": 0 + }; }; Player.prototype.SetPlayerID = function(id) @@ -24,6 +30,16 @@ Player.prototype.GetPopulationLimit = function() return this.popLimit; }; +Player.prototype.GetResourceCounts = function() +{ + return this.resourceCount; +}; + +Player.prototype.AddResources = function(type, amount) +{ + this.resourceCount[type] += (+amount); +}; + Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from == this.playerID) diff --git a/binaries/data/mods/public/simulation/components/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/ResourceGatherer.js new file mode 100644 index 0000000000..b680d4d0b1 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -0,0 +1,49 @@ +function ResourceGatherer() {} + +ResourceGatherer.prototype.Init = function() +{ +}; + +ResourceGatherer.prototype.GetGatherRates = function() +{ + var ret = {}; + for (var r in this.template.Rates) + ret[r] = this.template.Rates[r] * this.template.BaseSpeed; + return ret; +}; + +ResourceGatherer.prototype.GetRange = function() +{ + return { "max": 4, "min": 0 }; + // maybe this should depend on the unit or target or something? +} + +/** + * Gather from the target entity. This should only be called after a successful range check, + * and if the target has a compatible ResourceSupply. + * It should be called at a rate of once per second. + */ +ResourceGatherer.prototype.PerformGather = function(target) +{ + var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); + var type = cmpResourceSupply.GetType(); + + var rate; + if (type.specific && this.template.Rates[type.generic+"."+type.specific]) + rate = this.template.Rates[type.generic+"."+type.specific] * this.template.BaseSpeed; + else + rate = this.template.Rates[type.generic] * this.template.BaseSpeed; + + var status = cmpResourceSupply.TakeResources(rate); + + // Give the gathered resources to the player + var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(cmpOwnership.GetOwner()), IID_Player); + cmpPlayer.AddResources(type.generic, status.amount); + + return status; +}; + + +Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer); diff --git a/binaries/data/mods/public/simulation/components/ResourceSupply.js b/binaries/data/mods/public/simulation/components/ResourceSupply.js new file mode 100644 index 0000000000..4fdcb97fae --- /dev/null +++ b/binaries/data/mods/public/simulation/components/ResourceSupply.js @@ -0,0 +1,42 @@ +function ResourceSupply() {} + +ResourceSupply.prototype.Init = function() +{ + // Current resource amount (non-negative; can be a fractional amount) + this.amount = this.GetMaxAmount(); +}; + +ResourceSupply.prototype.GetMaxAmount = function() +{ + return +this.template.Amount; +}; + +ResourceSupply.prototype.GetCurrentAmount = function() +{ + return this.amount; +}; + +ResourceSupply.prototype.TakeResources = function(rate) +{ + // Internally we handle fractional resource amounts (to be accurate + // over long periods of time), but want to return integers (so players + // have a nice simple integer amount of resources). So return the + // difference between rounded values: + + var old = this.amount; + this.amount = Math.max(0, old - rate/1000); + var change = Math.ceil(old) - Math.ceil(this.amount); + // (use ceil instead of floor so that we continue returning non-zero values even if + // 0 < amount < 1) + return { "amount": change, "exhausted": (old == 0) }; +}; + +ResourceSupply.prototype.GetType = function() +{ + if (this.template.Subtype) + return { "generic": this.template.Type, "specific": this.template.Subtype }; + else + return { "generic": this.template.Type }; +}; + +Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply); diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 476672eb44..3001dfe7c1 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -1,13 +1,14 @@ /* This is currently just a very simplistic state machine that lets units be commanded around -and then autonomously carry out the orders. +and then autonomously carry out the orders. It might need to be entirely redesigned. */ const STATE_IDLE = 0; const STATE_WALKING = 1; const STATE_ATTACKING = 2; +const STATE_GATHERING = 3; /* Attack process: * When starting attack: @@ -25,6 +26,11 @@ const STATE_ATTACKING = 2; * faster-than-normal attacks) */ +/* Gather process is about the same, except with less synchronisation - the action + * is just performed 1sec after initiated, and then repeated every 1sec. + * (TODO: it'd be nice to avoid most of the duplication between Attack and Gather code) + */ + function UnitAI() {} UnitAI.prototype.Init = function() @@ -38,6 +44,11 @@ UnitAI.prototype.Init = function() this.attackTimer = undefined; // Current target entity ID this.attackTarget = undefined; + + // Timer for GatherTimeout + this.gatherTimer = undefined; + // Current target entity ID + this.gatherTarget = undefined; }; //// Interface functions //// @@ -57,33 +68,46 @@ UnitAI.prototype.Walk = function(x, z) UnitAI.prototype.Attack = function(target) { + // Verify that we're able to respond to Attack commands var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; + // TODO: verify that this is a valid target + + // Stop any previous action timers + this.CancelTimers(); + // Remember the target, and start moving towards it this.attackTarget = target; - this.MoveToTarget(this.attackTarget); + this.MoveToTarget(target, cmpAttack.GetRange()); this.state = STATE_ATTACKING; +}; - // Cancel any previous attack timer - if (this.attackTimer) - { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.attackTimer); - this.attackTimer = undefined; - } +UnitAI.prototype.Gather = function(target) +{ + // Verify that we're able to respond to Gather commands + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (!cmpResourceGatherer) + return; + + // TODO: verify that this is a valid target + + // Stop any previous action timers + this.CancelTimers(); + + // Remember the target, and start moving towards it + this.gatherTarget = target; + this.MoveToTarget(target, cmpResourceGatherer.GetRange()); + this.state = STATE_GATHERING; }; //// Message handlers //// UnitAI.prototype.OnDestroy = function() { - if (this.attackTimer) - { - cmpTimer.CancelTimer(this.attackTimer); - this.attackTimer = undefined; - } + // Clean up any timers that are now obsolete + this.CancelTimers(); }; UnitAI.prototype.OnMotionChanged = function(msg) @@ -123,11 +147,78 @@ UnitAI.prototype.OnMotionChanged = function(msg) // Start the idle animation before we switch to the attack this.SelectAnimation("idle"); } + else if (this.state == STATE_GATHERING) + { + // We were gathering, and have stopped moving + // => check if we can still reach the target now + + if (!this.MoveIntoGatherRange()) + return; + + // In range, so perform the gathering + + var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply); + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + + this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {}); + + // Start the gather animation + var type = cmpResourceSupply.GetType(); + var anim = "gather_" + (type.specific || type.generic); + this.SelectAnimation(anim); + } } }; //// Private functions //// +function hypot2(x, y) +{ + return x*x + y*y; +} + +UnitAI.prototype.CheckRange = function(target, range) +{ + // Target must be in the world + var cmpPositionTarget = Engine.QueryInterface(target, IID_Position); + if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) + return { "error": "not-in-world" }; + + // We must be in the world + var cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld()) + return { "error": "not-in-world" }; + + // Target must be within range + var posTarget = cmpPositionTarget.GetPosition(); + var posSelf = cmpPositionSelf.GetPosition(); + var dist2 = hypot2(posTarget.x - posSelf.x, posTarget.z - posSelf.z); + // TODO: ought to be distance to closest point in footprint, not to center + // The +4 is a hack to give a ~1 tile tolerance, because the pathfinder doesn't + // always get quite close enough to the target + if (dist2 > (range.max+4)*(range.max+4)) + return { "error": "out-of-range" }; + + return {}; +} + +UnitAI.prototype.CancelTimers = function() +{ + if (this.attackTimer) + { + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.attackTimer); + this.attackTimer = undefined; + } + + if (this.gatherTimer) + { + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.gatherTimer); + this.gatherTimer = undefined; + } +}; + /** * Tries to move into range of the attack target. * Returns true if it's already in range. @@ -135,15 +226,42 @@ UnitAI.prototype.OnMotionChanged = function(msg) UnitAI.prototype.MoveIntoAttackRange = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + var range = cmpAttack.GetRange(); - var rangeStatus = cmpAttack.CheckRange(this.attackTarget); + var rangeStatus = this.CheckRange(this.attackTarget, range); if (rangeStatus.error) { if (rangeStatus.error == "out-of-range") { // Out of range => need to move closer // (The target has probably moved while we were chasing it) - this.MoveToTarget(this.attackTarget); + this.MoveToTarget(this.attackTarget, range); + return false; + } + + // Otherwise it's impossible to reach the target, so give up + // and switch back to idle + this.state = STATE_IDLE; + this.SelectAnimation("idle"); + return false; + } + + return true; +}; + +UnitAI.prototype.MoveIntoGatherRange = function() +{ + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + var range = cmpResourceGatherer.GetRange(); + + var rangeStatus = this.CheckRange(this.gatherTarget, range); + if (rangeStatus.error) + { + if (rangeStatus.error == "out-of-range") + { + // Out of range => need to move closer + // (The target has probably moved while we were chasing it) + this.MoveToTarget(this.gatherTarget, range); return false; } @@ -166,7 +284,7 @@ UnitAI.prototype.SelectAnimation = function(name, once, speed) cmpVisual.SelectAnimation(name, once, speed); }; -UnitAI.prototype.MoveToTarget = function(target) +UnitAI.prototype.MoveToTarget = function(target, range) { var cmpPositionTarget = Engine.QueryInterface(target, IID_Position); if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) @@ -175,7 +293,7 @@ UnitAI.prototype.MoveToTarget = function(target) var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); var pos = cmpPositionTarget.GetPosition(); - cmpMotion.MoveToPoint(pos.x, pos.z, 0, 1); + cmpMotion.MoveToPoint(pos.x, pos.z, range.min, range.max); }; UnitAI.prototype.AttackTimeout = function(data) @@ -205,4 +323,33 @@ UnitAI.prototype.AttackTimeout = function(data) this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat, data); }; +UnitAI.prototype.GatherTimeout = function(data) +{ + // If we stopped gathering before this timeout, then don't do any processing here + if (this.state != STATE_GATHERING) + return; + + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + + // Check if we can still reach the target + if (!this.MoveIntoGatherRange()) + return; + + // Gather from the target + var status = cmpResourceGatherer.PerformGather(this.gatherTarget); + + // If the resource is exhausted, then stop and go back to idle + if (status.exhausted) + { + this.state = STATE_IDLE; + this.SelectAnimation("idle"); + return; + } + + // Set a timer to gather again + + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, data); +}; + Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); diff --git a/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js new file mode 100644 index 0000000000..3c5be75d57 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js @@ -0,0 +1 @@ +Engine.RegisterInterface("ResourceGatherer"); diff --git a/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js b/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js new file mode 100644 index 0000000000..5b63e5ea10 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js @@ -0,0 +1 @@ +Engine.RegisterInterface("ResourceSupply"); diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js index cf592567ec..1e778e3478 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -1,6 +1,8 @@ Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Health.js"); +Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); +Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("GuiInterface.js"); var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); @@ -18,16 +20,18 @@ AddMock(SYSTEM_ENTITY, IID_TemplateManager, { AddMock(100, IID_Player, { GetPopulationCount: function() { return 10; }, - GetPopulationLimit: function() { return 20; } + GetPopulationLimit: function() { return 20; }, + GetResourceCounts: function() { return { "food": 100 }; } }); AddMock(101, IID_Player, { GetPopulationCount: function() { return 40; }, - GetPopulationLimit: function() { return 30; } + GetPopulationLimit: function() { return 30; }, + GetResourceCounts: function() { return { "food": 200 }; } }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { - players: [{popCount:10, popLimit:20}, {popCount:40, popLimit:30}] + players: [{popCount:10, popLimit:20, resourceCounts:{food:100}}, {popCount:40, popLimit:30, resourceCounts:{food:200}}] }); diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index ce4dc9b1e6..a82b5c13cd 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -24,6 +24,16 @@ function ProcessCommand(player, cmd) } break; + case "gather": + for each (var ent in cmd.entities) + { + var ai = Engine.QueryInterface(ent, IID_UnitAI); + if (!ai) + continue; + ai.Gather(cmd.target); + } + break; + case "construct": // TODO: this should do all sorts of stuff with foundations and resource costs etc var ent = Engine.AddEntity(cmd.template); diff --git a/source/maths/Fixed.h b/source/maths/Fixed.h index 6dfb4970ed..5d65e6be40 100644 --- a/source/maths/Fixed.h +++ b/source/maths/Fixed.h @@ -94,6 +94,9 @@ public: /// Equality. bool operator==(CFixed n) const { return (value == n.value); } + /// Inequality. + bool operator!=(CFixed n) const { return (value != n.value); } + /// Numeric comparison. bool operator<=(CFixed n) const { return (value <= n.value); } diff --git a/source/simulation2/components/CCmpUnitMotion.cpp b/source/simulation2/components/CCmpUnitMotion.cpp index 8a45ebbde5..4e17c12d06 100644 --- a/source/simulation2/components/CCmpUnitMotion.cpp +++ b/source/simulation2/components/CCmpUnitMotion.cpp @@ -44,7 +44,9 @@ public: bool m_HasTarget; ICmpPathfinder::Path m_Path; - entity_pos_t m_TargetX, m_TargetZ; // these values contain undefined junk if !HasTarget + // These values contain undefined junk if !HasTarget: + entity_pos_t m_TargetX, m_TargetZ; // currently-selected waypoint + entity_pos_t m_FinalTargetX, m_FinalTargetZ; // final target center (used to face towards it) enum { @@ -76,6 +78,7 @@ public: // TODO: m_Path serialize.NumberFixed_Unbounded("target x", m_TargetX); serialize.NumberFixed_Unbounded("target z", m_TargetZ); + // TODO: m_FinalTargetAngle } // TODO: m_State @@ -103,9 +106,9 @@ public: if (m_State == STOPPING) { + m_State = IDLE; CMessageMotionChanged msg(CFixed_23_8::FromInt(0)); context.GetComponentManager().PostMessage(GetEntityId(), msg); - m_State = IDLE; } Move(context, dt); @@ -194,6 +197,8 @@ public: } else { + m_FinalTargetX = x; + m_FinalTargetZ = z; PickNextWaypoint(pos); } } @@ -245,6 +250,16 @@ void CCmpUnitMotion::Move(const CSimContext& context, CFixed_23_8 dt) if (m_Path.m_Waypoints.empty()) { cmpPosition->MoveTo(target.X, target.Z); + + // If we didn't reach the final goal, point towards it now + if (target.X != m_FinalTargetX || target.Z != m_FinalTargetZ) + { + CFixedVector3D final(m_FinalTargetX, CFixed_23_8::FromInt(0), m_FinalTargetZ); + CFixedVector3D finalOffset = final - target; + entity_angle_t angle = atan2_approx(finalOffset.X, finalOffset.Z); + cmpPosition->TurnTo(angle); + } + m_HasTarget = false; SwitchState(context, IDLE); return;