Index: binaries/data/mods/public/simulation/components/Turretable.js =================================================================== --- binaries/data/mods/public/simulation/components/Turretable.js +++ binaries/data/mods/public/simulation/components/Turretable.js @@ -78,10 +78,6 @@ if (cmpUnitAI) cmpUnitAI.SetTurretStance(); - let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotion) - cmpUnitMotion.SetFacePointAfterMove(false); - // Remove the unit's obstruction to avoid interfering with pathing. let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) @@ -115,10 +111,6 @@ if (!cmpTurretHolder || !cmpTurretHolder.LeaveTurretPoint(this.entity, forced)) return false; - let cmpUnitMotionEntity = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotionEntity) - cmpUnitMotionEntity.SetFacePointAfterMove(true); - let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) { Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -1010,6 +1010,11 @@ this.StopTimer(); }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Timer": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) @@ -1170,6 +1175,11 @@ delete this.stopSurveying; }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Timer": function(msg) { if (this.stopSurveying >= +this.template.PatrolWaitTime) { @@ -1222,6 +1232,7 @@ "GARRISONING": { "enter": function() { + this.StopMoving(); this.CallMemberFunction(this.order.data.garrison ? "Garrison" : "OccupyTurret", [this.order.data.target, false]); // We might have been disbanded due to the lack of members. if (Engine.QueryInterface(this.entity, IID_Formation).GetMemberCount()) @@ -1314,6 +1325,11 @@ return false; }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Timer": function(msg) { let target = this.order.data.target; let allowCapture = this.order.data.allowCapture; @@ -1406,10 +1422,8 @@ "FormationLeave": function(msg) { // Stop moving as soon as the formation disbands // Keep current rotation - let facePointAfterMove = this.GetFacePointAfterMove(); - this.SetFacePointAfterMove(false); - this.StopMoving(); - this.SetFacePointAfterMove(facePointAfterMove); + this.StopMoving(false); + // If the controller handled an order but some members rejected it, // they will have no orders and be in the FORMATIONMEMBER.IDLE state. @@ -1477,13 +1491,10 @@ }, "leave": function() { - // Don't use the logic from unitMotion, as SetInPosition + // Don't try to rotate to the target as the formation // has already given us a custom rotation // (or we failed to move and thus don't care.) - let facePointAfterMove = this.GetFacePointAfterMove(); - this.SetFacePointAfterMove(false); - this.StopMoving(); - this.SetFacePointAfterMove(facePointAfterMove); + this.StopMoving(false); }, // Occurs when the unit has reached its destination and the controller @@ -1653,6 +1664,11 @@ this.AttackEntitiesByPreference(msg.data.added); }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Timer": function(msg) { if (this.isGuardOf) { @@ -1706,16 +1722,14 @@ "ROAMING": { "enter": function() { - this.SetFacePointAfterMove(false); this.MoveRandomly(+this.template.RoamDistance); this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); return false; }, "leave": function() { - this.StopMoving(); + this.StopMoving(false); this.StopTimer(); - this.SetFacePointAfterMove(true); }, "Timer": function(msg) { @@ -1740,6 +1754,11 @@ this.StopTimer(); }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Timer": function(msg) { this.SetNextState("ROAMING"); }, @@ -1876,6 +1895,11 @@ delete this.stopSurveying; }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Timer": function(msg) { if (this.stopSurveying >= +this.template.PatrolWaitTime) { @@ -3367,6 +3391,11 @@ cmpPack.CancelPack(); }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Attacked": function(msg) { // Ignore attacks while packing }, @@ -3393,6 +3422,11 @@ cmpPack.CancelPack(); }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "Attacked": function(msg) { // Ignore attacks while unpacking }, @@ -3434,6 +3468,11 @@ return false; }, + "MovementUpdate": function(msg) { + // We could be rotating, stop it when it is done. + this.StopMoving(false); + }, + "PickupCanceled": function() { this.FinishOrder(); }, @@ -3555,6 +3594,8 @@ if (this.isImmobile) return; + this.StopMoving(false); + this.isImmobile = true; Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, { "entity": this.entity, @@ -4637,11 +4678,9 @@ cmpVisual.SetAnimationSyncOffset(actiontime); }; -UnitAI.prototype.StopMoving = function() +UnitAI.prototype.StopMoving = function(faceToTarget = true) { - let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpUnitMotion) - cmpUnitMotion.StopMoving(); + Engine.QueryInterface(this.entity, IID_UnitMotion)?.StopMoving(faceToTarget); }; /** @@ -6393,19 +6432,6 @@ cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1); }; -UnitAI.prototype.SetFacePointAfterMove = function(val) -{ - var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - if (cmpMotion) - cmpMotion.SetFacePointAfterMove(val); -}; - -UnitAI.prototype.GetFacePointAfterMove = function() -{ - let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion && cmpUnitMotion.GetFacePointAfterMove(); -}; - UnitAI.prototype.AttackEntitiesByPreference = function(ents) { if (!ents.length) Index: binaries/data/mods/public/simulation/components/UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitMotionFlying.js +++ binaries/data/mods/public/simulation/components/UnitMotionFlying.js @@ -370,11 +370,6 @@ // 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. Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -175,8 +175,6 @@ "MoveToTargetRange": (target, min, max) => true, "SetMemberOfFormation": () => {}, "StopMoving": () => {}, - "SetFacePointAfterMove": () => {}, - "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); @@ -248,8 +246,6 @@ "SetSpeedMultiplier": () => {}, "SetAcceleration": (accel) => {}, "MoveToPointRange": () => true, - "SetFacePointAfterMove": () => {}, - "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); @@ -357,8 +353,6 @@ "MoveToTargetRange": (target, min, max) => true, "SetMemberOfFormation": () => {}, "StopMoving": () => {}, - "SetFacePointAfterMove": () => {}, - "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); @@ -422,8 +416,6 @@ "SetAcceleration": (accel) => {}, "MoveToPointRange": (x, z, minRange, maxRange) => {}, "StopMoving": () => {}, - "SetFacePointAfterMove": () => {}, - "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); Index: source/simulation2/components/CCmpUnitMotion.h =================================================================== --- source/simulation2/components/CCmpUnitMotion.h +++ source/simulation2/components/CCmpUnitMotion.h @@ -166,8 +166,6 @@ // cached for efficiency fixed m_WalkSpeed, m_RunMultiplier; - bool m_FacePointAfterMove; - // Whether the unit participates in pushing. bool m_Pushing = false; @@ -204,7 +202,8 @@ NONE, POINT, ENTITY, - OFFSET + OFFSET, + ROTATEPOINT } m_Type = NONE; entity_id_t m_Entity = INVALID_ENTITY; CFixedVector2D m_Position; @@ -217,6 +216,7 @@ MoveRequest(CFixedVector2D pos, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(POINT), m_Position(pos), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(ENTITY), m_Entity(target), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {}; + MoveRequest(CFixedVector2D pos) : m_Type(ROTATEPOINT), m_Position(pos) {}; } m_MoveRequest; // If this is not INVALID_ENTITY, the unit is a formation member. @@ -284,8 +284,6 @@ { m_IsFormationController = paramNode.GetChild("FormationController").ToBool(); - m_FacePointAfterMove = true; - m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); m_SpeedMultiplier = fixed::FromInt(1); m_LastTurnSpeed = m_CurrentSpeed = fixed::Zero(); @@ -353,7 +351,6 @@ serialize.NumberFixed_Unbounded("acceleration", m_Acceleration); - serialize.Bool("facePointAfterMove", m_FacePointAfterMove); serialize.Bool("pushing", m_Pushing); Serializer(serialize, "long path", m_LongPath.m_Waypoints); @@ -530,16 +527,6 @@ return m_CurrentSpeed; } - virtual void SetFacePointAfterMove(bool facePointAfterMove) - { - m_FacePointAfterMove = facePointAfterMove; - } - - virtual bool GetFacePointAfterMove() const - { - return m_FacePointAfterMove; - } - virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; @@ -576,23 +563,24 @@ * This should never be called from UnitMotion, since MoveToX orders are given * by other components - these components should also decide when to stop. */ - virtual void StopMoving() + virtual void StopMoving(const bool faceToTarget) { - if (m_FacePointAfterMove) + m_ExpectedPathTicket.clear(); + m_LongPath.m_Waypoints.clear(); + m_ShortPath.m_Waypoints.clear(); + + if (faceToTarget) { - CmpPtr cmpPosition(GetEntityHandle()); - if (cmpPosition && cmpPosition->IsInWorld()) + CFixedVector2D targetPos; + if (ComputeTargetPosition(targetPos)) { - CFixedVector2D targetPos; - if (ComputeTargetPosition(targetPos)) - FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), targetPos.X, targetPos.Y); + FaceTowardsPoint(targetPos.X, targetPos.Y); + // Don't overwrite the new MoveRequest. + return; } } m_MoveRequest = MoveRequest(); - m_ExpectedPathTicket.clear(); - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); } virtual entity_pos_t GetUnitClearance() const @@ -811,11 +799,6 @@ bool PathingUpdateNeeded(const CFixedVector2D& from) const; /** - * Rotate to face towards the target point, given the current pos - */ - void FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z); - - /** * Units in 'pushing' mode are marked as 'moving' in the obstruction manager. * Units in 'pushing' mode should skip them in checkMovement (to enable pushing). * However, units for which pushing is deactivated should collide against everyone. @@ -901,7 +884,7 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { // Ignore obsolete path requests - if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE) + if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE || m_MoveRequest.m_Type == MoveRequest::ROTATEPOINT) return; Ticket::Type ticketType = m_ExpectedPathTicket.m_Type; @@ -1044,7 +1027,7 @@ // Update moving flag, this is an internal construct used for pushing, // so it does not really reflect whether the unit is actually moving or not. - state.isMoving = m_Pushing && m_MoveRequest.m_Type != MoveRequest::NONE; + state.isMoving = m_Pushing && m_MoveRequest.m_Type != MoveRequest::NONE && m_MoveRequest.m_Type != MoveRequest::ROTATEPOINT; CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetMovingFlag(state.isMoving); @@ -1081,6 +1064,9 @@ UpdateMovementState(state.speed, offset.Length() / dt); } + if (m_MoveRequest.m_Type == MoveRequest::ROTATEPOINT) + return; + if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos)) return; else if (!state.wasObstructed && state.pos != state.initialPos) @@ -1107,6 +1093,16 @@ if (m_MoveRequest.m_Type == MoveRequest::NONE) return false; + if (m_MoveRequest.m_Type == MoveRequest::ROTATEPOINT) + { + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return false; + + CFixedVector2D offset = m_MoveRequest.m_Position - cmpPosition->GetPosition2D(); + return offset.IsZero() || atan2_approx(offset.X, offset.Y) == cmpPosition->GetRotation().Y; + } + CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); @@ -1134,7 +1130,7 @@ bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const { // If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. - if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) + if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty() && m_MoveRequest.m_Type != MoveRequest::ROTATEPOINT) return true; // Wrap the angle to (-Pi, Pi]. @@ -1190,14 +1186,16 @@ while (timeLeft > zero) { // If we ran out of path, we have to stop. - if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) + if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty() && m_MoveRequest.m_Type != MoveRequest::ROTATEPOINT) break; CFixedVector2D target; - if (shortPath.m_Waypoints.empty()) + if (!shortPath.m_Waypoints.empty()) + target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); + else if (!longPath.m_Waypoints.empty()) target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z); else - target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); + target = m_MoveRequest.m_Position; CFixedVector2D offset = target - pos; @@ -1241,6 +1239,10 @@ } } + // We are done rotating. + if (m_MoveRequest.m_Type == MoveRequest::ROTATEPOINT) + break; + // Work out how far we can travel in timeLeft. fixed accelTime = std::min(timeLeft, (maxSpeed - speed) / m_Acceleration); fixed accelDist = speed.Multiply(accelTime) + accelTime.Square().Multiply(m_Acceleration) / 2; @@ -1332,6 +1334,11 @@ MoveObstructed(); } + // Check this after the moveObstructed messages, as they might change the moveRequest. + // We shouldn't request paths when just trying to rotate. + if (m_MoveRequest.m_Type == MoveRequest::ROTATEPOINT) + return false; + PathGoal goal; if (!ComputeGoal(goal, m_MoveRequest)) return false; @@ -1410,7 +1417,10 @@ bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const { - if (moveRequest.m_Type == MoveRequest::POINT) + if (moveRequest.m_Type == MoveRequest::NONE) + return false; + + if (moveRequest.m_Type == MoveRequest::POINT || moveRequest.m_Type == MoveRequest::ROTATEPOINT) { out = moveRequest.m_Position; return true; @@ -1453,6 +1463,9 @@ bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths) { + if (m_MoveRequest.m_Type == MoveRequest::ROTATEPOINT) + return false; + // Assume if we have short paths we want to follow them. // Exception: offset movement (formations) generally have very short deltas // and to look good we need them to walk-straight most of the time. @@ -1514,7 +1527,7 @@ bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const { - if (m_MoveRequest.m_Type == MoveRequest::NONE) + if (m_MoveRequest.m_Type == MoveRequest::NONE || m_MoveRequest.m_Type == MoveRequest::ROTATEPOINT) return false; CFixedVector2D targetPos; @@ -1576,27 +1589,8 @@ void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - FaceTowardsPointFromPos(pos, x, z); -} - -void CCmpUnitMotion::FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z) -{ CFixedVector2D target(x, z); - CFixedVector2D offset = target - pos; - if (!offset.IsZero()) - { - entity_angle_t angle = atan2_approx(offset.X, offset.Y); - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition) - return; - cmpPosition->TurnTo(angle); - } + MoveTo(MoveRequest(target)); } // The pathfinder cannot go to "rounded rectangles" goals, which are what happens with square targets and a non-null range. @@ -1617,6 +1611,9 @@ if (moveRequest.m_Type == MoveRequest::NONE) return false; + if (moveRequest.m_Type == MoveRequest::ROTATEPOINT) + return true; + CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; @@ -1833,7 +1830,9 @@ m_FailedMovements = 0; m_FollowKnownImperfectPathCountdown = 0; - ComputePathToGoal(cmpPosition->GetPosition2D(), goal); + if (request.m_Type != MoveRequest::ROTATEPOINT) + ComputePathToGoal(cmpPosition->GetPosition2D(), goal); + return true; } Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -84,7 +84,7 @@ /** * Stop moving immediately. */ - virtual void StopMoving() = 0; + virtual void StopMoving(const bool faceToTarget) = 0; /** * Get the speed at the end of the current turn. @@ -141,13 +141,6 @@ virtual void SetAcceleration(fixed acceleration) = 0; /** - * Set whether the unit will turn to face the target point after finishing moving. - */ - virtual void SetFacePointAfterMove(bool facePointAfterMove) = 0; - - virtual bool GetFacePointAfterMove() const = 0; - - /** * Get the unit's passability class. */ virtual pass_class_t GetPassabilityClass() const = 0; Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -41,8 +41,6 @@ DEFINE_INTERFACE_METHOD("SetAcceleration", ICmpUnitMotion, SetAcceleration) DEFINE_INTERFACE_METHOD("GetPassabilityClassName", ICmpUnitMotion, GetPassabilityClassName) DEFINE_INTERFACE_METHOD("GetUnitClearance", ICmpUnitMotion, GetUnitClearance) -DEFINE_INTERFACE_METHOD("SetFacePointAfterMove", ICmpUnitMotion, SetFacePointAfterMove) -DEFINE_INTERFACE_METHOD("GetFacePointAfterMove", ICmpUnitMotion, GetFacePointAfterMove) DEFINE_INTERFACE_METHOD("SetDebugOverlay", ICmpUnitMotion, SetDebugOverlay) END_INTERFACE_WRAPPER(UnitMotion) @@ -81,9 +79,9 @@ m_Script.CallVoid("FaceTowardsPoint", x, z); } - virtual void StopMoving() + virtual void StopMoving(const bool faceToTarget) { - m_Script.CallVoid("StopMoving"); + m_Script.CallVoid("StopMoving", faceToTarget); } virtual fixed GetCurrentSpeed() const @@ -136,16 +134,6 @@ m_Script.CallVoid("SetAcceleration", acceleration); } - virtual void SetFacePointAfterMove(bool facePointAfterMove) - { - m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove); - } - - virtual bool GetFacePointAfterMove() const - { - return m_Script.Call("GetFacePointAfterMove"); - } - virtual pass_class_t GetPassabilityClass() const { return m_Script.Call("GetPassabilityClass");