diff --git a/binaries/data/mods/public/simulation/components/Formation.js b/binaries/data/mods/public/simulation/components/Formation.js index 8341ccc83f..cfdb194463 100644 --- a/binaries/data/mods/public/simulation/components/Formation.js +++ b/binaries/data/mods/public/simulation/components/Formation.js @@ -389,7 +389,9 @@ Formation.prototype.RemoveMembers = function(ents, renamed = false) if (!ents.length) return; - this.offsets = undefined; + if (!renamed) + this.offsets = undefined; + this.members = this.members.filter(ent => !ents.includes(ent)); for (const ent of ents) @@ -441,9 +443,10 @@ Formation.prototype.RemoveMembers = function(ents, renamed = false) * * @see ArrangeFormation - To update formation layout after adding members */ -Formation.prototype.AddMembers = function(ents) +Formation.prototype.AddMembers = function(ents, renamed = false) { - this.offsets = undefined; + if (!renamed) + this.offsets = undefined; for (const ent of this.formationMembersWithAura) { @@ -1048,11 +1051,24 @@ Formation.prototype.OnGlobalEntityRenamed = function(msg) // First remove the old member to be able to reuse its position. this.RemoveMembers([msg.entity], true); - this.AddMembers([msg.newentity]); + this.AddMembers([msg.newentity], true); this.memberPositions[msg.newentity] = this.memberPositions[msg.entity]; + delete this.memberPositions[msg.entity]; - // Update Formation - // to make sure added (renamed) members will move with the controller if applicable. + if (this.resetOffsetsScheduled === undefined) + { + this.resetOffsetsScheduled = true; + + // Schedule offset reset for the next tick so that it reorders if necessary. + const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.SetTimeout(this.entity, IID_Formation, "ResetOffsetsAndUpdate", 0, null); + } +}; + +Formation.prototype.ResetOffsetsAndUpdate = function() +{ + this.resetOffsetsScheduled = undefined; + this.offsets = undefined; this.UpdateFormation(false, false); }; diff --git a/binaries/data/mods/public/simulation/helpers/Transform.js b/binaries/data/mods/public/simulation/helpers/Transform.js index cacd668cce..19f2d8ece1 100644 --- a/binaries/data/mods/public/simulation/helpers/Transform.js +++ b/binaries/data/mods/public/simulation/helpers/Transform.js @@ -62,6 +62,7 @@ function ChangeEntityTemplate(oldEnt, newTemplate) const cmpUnitMotion = Engine.QueryInterface(oldEnt, IID_UnitMotion); const cmpNewUnitMotion = Engine.QueryInterface(newEnt, IID_UnitMotion); + const cmpOldUnitAI = Engine.QueryInterface(oldEnt, IID_UnitAI); if (cmpUnitMotion && cmpNewUnitMotion) { const currentSpeed = cmpUnitMotion.GetCurrentSpeed(); @@ -69,6 +70,13 @@ function ChangeEntityTemplate(oldEnt, newTemplate) const acceleration = cmpUnitMotion.GetAcceleration(); cmpNewUnitMotion.SetAcceleration(acceleration); + if (cmpOldUnitAI) + { + const formationControllerID = cmpOldUnitAI.GetFormationController(); + const formationMemberOffsetPosition = cmpUnitMotion.GetFormationOffset(); + if (formationControllerID && formationMemberOffsetPosition && cmpUnitMotion.IsMovingAsFormation()) + cmpNewUnitMotion.MoveToFormationOffset(formationControllerID, formationMemberOffsetPosition.x, formationMemberOffsetPosition.y); + } } // Prevent spawning subunits on occupied positions. @@ -172,16 +180,15 @@ function ChangeEntityTemplate(oldEnt, newTemplate) Engine.PostMessage(oldEnt, MT_EntityRenamed, { "entity": oldEnt, "newentity": newEnt }); // UnitAI generally needs other components to be properly initialised. - const cmpUnitAI = Engine.QueryInterface(oldEnt, IID_UnitAI); const cmpNewUnitAI = Engine.QueryInterface(newEnt, IID_UnitAI); - if (cmpUnitAI && cmpNewUnitAI) + if (cmpOldUnitAI && cmpNewUnitAI) { - const pos = cmpUnitAI.GetHeldPosition(); + const pos = cmpOldUnitAI.GetHeldPosition(); if (pos) cmpNewUnitAI.SetHeldPosition(pos.x, pos.z); - cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName()); - cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders()); - const guarded = cmpUnitAI.IsGuardOf(); + cmpNewUnitAI.SwitchToStance(cmpOldUnitAI.GetStanceName()); + cmpNewUnitAI.AddOrders(cmpOldUnitAI.GetOrders()); + const guarded = cmpOldUnitAI.IsGuardOf(); if (guarded) { const cmpGuarded = Engine.QueryInterface(guarded, IID_Guard); diff --git a/source/simulation2/components/CCmpUnitMotion.h b/source/simulation2/components/CCmpUnitMotion.h index 25be7a0742..eb4767b7a8 100644 --- a/source/simulation2/components/CCmpUnitMotion.h +++ b/source/simulation2/components/CCmpUnitMotion.h @@ -456,6 +456,11 @@ public: return m_MoveRequest.m_Type != MoveRequest::NONE; } + bool IsMovingAsFormation() const override + { + return IsFormationMember() && m_MoveRequest.m_Type == MoveRequest::OFFSET; + } + fixed GetSpeedMultiplier() const override { return m_SpeedMultiplier; @@ -588,6 +593,14 @@ public: m_FormationController = controller; } + std::optional GetFormationOffset() const override + { + if (m_MoveRequest.m_Type != MoveRequest::OFFSET) + return std::nullopt; + + return m_MoveRequest.m_Position; + } + bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) override; void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) override; @@ -627,11 +640,6 @@ private: return m_FormationController != INVALID_ENTITY; } - bool IsMovingAsFormation() const - { - return IsFormationMember() && m_MoveRequest.m_Type == MoveRequest::OFFSET; - } - bool IsFormationControllerMoving() const { CmpPtr cmpControllerMotion(GetSimContext(), m_FormationController); diff --git a/source/simulation2/components/ICmpUnitMotion.cpp b/source/simulation2/components/ICmpUnitMotion.cpp index 750c853e15..94d7955f39 100644 --- a/source/simulation2/components/ICmpUnitMotion.cpp +++ b/source/simulation2/components/ICmpUnitMotion.cpp @@ -32,8 +32,10 @@ DEFINE_INTERFACE_METHOD("PossiblyAtDestination", ICmpUnitMotion, PossiblyAtDesti DEFINE_INTERFACE_METHOD("FaceTowardsPoint", ICmpUnitMotion, FaceTowardsPoint) DEFINE_INTERFACE_METHOD("StopMoving", ICmpUnitMotion, StopMoving) DEFINE_INTERFACE_METHOD("GetCurrentSpeed", ICmpUnitMotion, GetCurrentSpeed) +DEFINE_INTERFACE_METHOD("GetFormationOffset", ICmpUnitMotion, GetFormationOffset) DEFINE_INTERFACE_METHOD("SetCurrentSpeed", ICmpUnitMotion, SetCurrentSpeed) DEFINE_INTERFACE_METHOD("IsMoveRequested", ICmpUnitMotion, IsMoveRequested) +DEFINE_INTERFACE_METHOD("IsMovingAsFormation", ICmpUnitMotion, IsMovingAsFormation) DEFINE_INTERFACE_METHOD("GetSpeed", ICmpUnitMotion, GetSpeed) DEFINE_INTERFACE_METHOD("GetWalkSpeed", ICmpUnitMotion, GetWalkSpeed) DEFINE_INTERFACE_METHOD("GetRunMultiplier", ICmpUnitMotion, GetRunMultiplier) @@ -104,11 +106,21 @@ public: m_Script.CallVoid("SetCurrentSpeed", speed); } + std::optional GetFormationOffset() const override + { + return m_Script.Call>("GetFormationOffset"); + } + bool IsMoveRequested() const override { return m_Script.Call("IsMoveRequested"); } + bool IsMovingAsFormation() const override + { + return m_Script.Call("IsMovingAsFormation"); + } + fixed GetSpeed() const override { return m_Script.Call("GetSpeed"); diff --git a/source/simulation2/components/ICmpUnitMotion.h b/source/simulation2/components/ICmpUnitMotion.h index ce0a3c12c9..d092a5f6e1 100644 --- a/source/simulation2/components/ICmpUnitMotion.h +++ b/source/simulation2/components/ICmpUnitMotion.h @@ -109,11 +109,23 @@ public: */ virtual void SetCurrentSpeed(const fixed& speed) = 0; + /** + * Get the current formation offset if this unit is moving as a formation member. + * @returns std::nullopt if the unit is not in formation movement mode, + * otherwise returns the formation offset. + */ + virtual std::optional GetFormationOffset() const = 0; + /** * @returns true if the unit has a destination. */ virtual bool IsMoveRequested() const = 0; + /** + * @returns true if the unit is moving orderly in it's Formation. + */ + virtual bool IsMovingAsFormation() const = 0; + /** * Get the unit template walk speed after modifications. */ diff --git a/source/simulation2/scripting/EngineScriptConversions.cpp b/source/simulation2/scripting/EngineScriptConversions.cpp index 183093851c..5ab171264d 100644 --- a/source/simulation2/scripting/EngineScriptConversions.cpp +++ b/source/simulation2/scripting/EngineScriptConversions.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -48,6 +48,7 @@ #include #include #include +#include #define FAIL(msg) STMT(LOGERROR(msg); return false) #define FAIL_VOID(msg) STMT(ScriptException::Raise(rq, msg); return) @@ -210,6 +211,30 @@ template<> void Script::ToJSVal(const ScriptRequest& rq, JS::Mu ret.setObject(*objVec); } +template<> bool Script::FromJSVal>(const ScriptRequest& rq, JS::HandleValue v, std::optional& out) +{ + if (v.isNullOrUndefined()) + { + out = std::nullopt; + return true; + } + + CFixedVector2D vec; + if (!FromJSVal(rq, v, vec)) + return false; + + out = vec; + return true; +} + +template<> void Script::ToJSVal>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::optional& val) +{ + if (!val.has_value()) + ret.setNull(); + else + ToJSVal(rq, ret, val.value()); +} + template<> void Script::ToJSVal >(const ScriptRequest& rq, JS::MutableHandleValue ret, const Grid& val) { u32 length = (u32)(val.m_W * val.m_H);