forked from mirrors/0ad
Simplify calls for moving units into formation
Co-authored-by: @Vantha Centralize all calls to move units into formation in UnitAI Allowing additional checks to be done before calling members to reform formation Fixes : #8545 Revised Formation methods and when they do call on reforming formation Fixes : #7328 Don't order formation members to go to a point if the controller is stuck Instead make the controller jump to the member who is closest to the destination Fixes : #8543 Correct 'filter(member)' instead of 'filter(ent)' in GetClosestMemberToPosition (ex GetClosestMember) Fixes : #5120
This commit is contained in:
@@ -90,9 +90,7 @@ Formation.prototype.variablesToSerialize = [
|
||||
"maxRowsUsed",
|
||||
"maxColumnsUsed",
|
||||
"finishedEntities",
|
||||
"idleEntities",
|
||||
"columnar",
|
||||
"rearrange",
|
||||
"formationMembersWithAura",
|
||||
"width",
|
||||
"depth",
|
||||
@@ -146,11 +144,8 @@ Formation.prototype.Init = function(deserialized = false)
|
||||
this.maxColumnsUsed = [];
|
||||
// Entities that have finished the original task.
|
||||
this.finishedEntities = new Set();
|
||||
this.idleEntities = new Set();
|
||||
// Whether we're travelling in column (vs box) formation.
|
||||
this.columnar = false;
|
||||
// Whether we should rearrange all formation members.
|
||||
this.rearrange = true;
|
||||
// Members with a formation aura.
|
||||
this.formationMembersWithAura = [];
|
||||
this.width = 0;
|
||||
@@ -163,7 +158,7 @@ Formation.prototype.Init = function(deserialized = false)
|
||||
return;
|
||||
|
||||
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
|
||||
.SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null);
|
||||
.SetInterval(this.entity, IID_Formation, "UpdateTwinFormationsForMerge", 1000, 1000, null);
|
||||
};
|
||||
|
||||
Formation.prototype.Serialize = function()
|
||||
@@ -210,26 +205,33 @@ Formation.prototype.GetMembers = function()
|
||||
return this.members;
|
||||
};
|
||||
|
||||
Formation.prototype.GetClosestMember = function(ent, filter)
|
||||
/**
|
||||
* Finds the closest formation member to a given position.
|
||||
*
|
||||
* @param {Vector2D} position - The 2D position to find the closest formation member to
|
||||
* @param {function} [filter] - Optional filter function that takes an entity ID and
|
||||
* returns true if the entity should be considered
|
||||
* @returns {number} Entity ID of the closest formation member, or INVALID_ENTITY
|
||||
* if no suitable member is found
|
||||
*/
|
||||
Formation.prototype.GetClosestMemberToPosition = function(targetPosition, filter)
|
||||
{
|
||||
const cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
if (!cmpEntPosition || !cmpEntPosition.IsInWorld())
|
||||
return INVALID_ENTITY;
|
||||
|
||||
const entPosition = cmpEntPosition.GetPosition2D();
|
||||
let closestMember = INVALID_ENTITY;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (const member of this.members)
|
||||
{
|
||||
if (filter && !filter(ent))
|
||||
if (filter && !filter(member))
|
||||
continue;
|
||||
|
||||
const cmpPosition = Engine.QueryInterface(member, IID_Position);
|
||||
if (!cmpPosition || !cmpPosition.IsInWorld())
|
||||
const cmpMemberPosition = Engine.QueryInterface(member, IID_Position);
|
||||
if (!cmpMemberPosition || !cmpMemberPosition.IsInWorld())
|
||||
continue;
|
||||
|
||||
const pos = cmpPosition.GetPosition2D();
|
||||
const dist = entPosition.distanceToSquared(pos);
|
||||
const memberPos = cmpMemberPosition.GetPosition2D();
|
||||
const memberPositionVector = new Vector2D(memberPos.x, memberPos.y);
|
||||
const targetPositionVector = new Vector2D(targetPosition.x, targetPosition.y);
|
||||
const dist = targetPositionVector.distanceToSquared(memberPositionVector);
|
||||
if (dist < closestDistance)
|
||||
{
|
||||
closestMember = member;
|
||||
@@ -239,6 +241,25 @@ Formation.prototype.GetClosestMember = function(ent, filter)
|
||||
return closestMember;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the closest formation member to a given entity.
|
||||
*
|
||||
* @param {number} ent - The entity ID to find the closest formation member to
|
||||
* @param {function} [filter] - Optional filter function that takes an entity ID and
|
||||
* returns true if the entity should be considered
|
||||
* @returns {number} Entity ID of the closest formation member, or INVALID_ENTITY
|
||||
* if no suitable member is found or the reference entity is invalid
|
||||
*/
|
||||
Formation.prototype.GetClosestMemberToEntity = function(ent, filter)
|
||||
{
|
||||
const cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
if (!cmpEntPosition || !cmpEntPosition.IsInWorld())
|
||||
return INVALID_ENTITY;
|
||||
|
||||
const entPosition = cmpEntPosition.GetPosition2D();
|
||||
return this.GetClosestMemberToPosition(entPosition, filter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the 'primary' member of this formation (typically the most
|
||||
* important unit type), for e.g. playing a representative sound.
|
||||
@@ -320,34 +341,6 @@ Formation.prototype.AreAllMembersFinished = function()
|
||||
return this.finishedEntities.size === this.members.length;
|
||||
};
|
||||
|
||||
Formation.prototype.SetIdleEntity = function(ent)
|
||||
{
|
||||
this.idleEntities.add(ent);
|
||||
};
|
||||
|
||||
Formation.prototype.UnsetIdleEntity = function(ent)
|
||||
{
|
||||
this.idleEntities.delete(ent);
|
||||
};
|
||||
|
||||
Formation.prototype.ResetIdleEntities = function()
|
||||
{
|
||||
this.idleEntities.clear();
|
||||
};
|
||||
|
||||
Formation.prototype.AreAllMembersIdle = function()
|
||||
{
|
||||
return this.idleEntities.size === this.members.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether we are allowed to rearrange formation members.
|
||||
*/
|
||||
Formation.prototype.SetRearrange = function(rearrange)
|
||||
{
|
||||
this.rearrange = rearrange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the members of this formation.
|
||||
* Must only be called once.
|
||||
@@ -379,10 +372,16 @@ Formation.prototype.SetMembers = function(ents)
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the given list of entities.
|
||||
* Removes entities from the formation member list.
|
||||
* The entities must already be members of this formation.
|
||||
*
|
||||
* Formation geometry and member positions are not recalculated.
|
||||
* As we let UnitAI define proper times and conditions to do so.
|
||||
* This avoids reordering in situations we don't want it to happen like in combat.
|
||||
*
|
||||
* @param {Array} ents - Array of entity IDs to remove from the formation
|
||||
* @param {boolean} rename - Whether the removal was part of an entity rename
|
||||
(prevents disbanding of the formation when under the member limit).
|
||||
* (prevents disbanding when under the member limit)
|
||||
*/
|
||||
Formation.prototype.RemoveMembers = function(ents, renamed = false)
|
||||
{
|
||||
@@ -412,23 +411,31 @@ Formation.prototype.RemoveMembers = function(ents, renamed = false)
|
||||
|
||||
this.formationMembersWithAura = this.formationMembersWithAura.filter(ent => !ents.includes(ent));
|
||||
|
||||
if (renamed)
|
||||
return;
|
||||
|
||||
// If there's nobody left, destroy the formation
|
||||
// unless this is a rename where we can have 0 members temporarily.
|
||||
if (!renamed && this.members.length < +this.template.RequiredMemberCount)
|
||||
if (this.members.length < +this.template.RequiredMemberCount)
|
||||
{
|
||||
this.Disband();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ComputeMotionParameters();
|
||||
|
||||
if (!this.rearrange)
|
||||
return;
|
||||
|
||||
// Rearrange the remaining members.
|
||||
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds entities to the formation member list.
|
||||
*
|
||||
* Formation geometry and member positions are not recalculated.
|
||||
* As we let UnitAI define proper times and conditions to do so.
|
||||
* This avoids reordering in situations we don't want it to happen like in combat.
|
||||
*
|
||||
* @param {Array} ents - Array of entity IDs to add to the formation
|
||||
*
|
||||
* @see ArrangeFormation - To update formation layout after adding members
|
||||
*/
|
||||
Formation.prototype.AddMembers = function(ents)
|
||||
{
|
||||
this.offsets = undefined;
|
||||
@@ -457,11 +464,6 @@ Formation.prototype.AddMembers = function(ents)
|
||||
}
|
||||
|
||||
this.ComputeMotionParameters();
|
||||
|
||||
if (!this.rearrange)
|
||||
return;
|
||||
|
||||
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -487,7 +489,7 @@ Formation.prototype.Disband = function()
|
||||
* otherwise the order to walk into formation is just pushed to the front.
|
||||
* @param {string | undefined} variant - Variant to be passed as order parameter.
|
||||
*/
|
||||
Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant)
|
||||
Formation.prototype.ArrangeFormation = function(moveCenter, force, variant)
|
||||
{
|
||||
if (!this.members.length)
|
||||
return;
|
||||
@@ -931,21 +933,41 @@ Formation.prototype.ComputeMotionParameters = function()
|
||||
cmpUnitMotion.SetPassabilityClassName(maxPassClass);
|
||||
};
|
||||
|
||||
Formation.prototype.ShapeUpdate = function()
|
||||
Formation.prototype.UpdateTwinFormationsForMerge = function()
|
||||
{
|
||||
if (!this.rearrange)
|
||||
const cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
|
||||
|
||||
// If one of the formations is idle, we don't want to merge into that one.
|
||||
// Because the formation controller should keep moving towards the destination
|
||||
// instead of staying at middle point.
|
||||
if (!this.IsRearrangementAllowed() || cmpUnitAI.isIdle)
|
||||
return;
|
||||
|
||||
const thisSize = this.GetSize();
|
||||
const thisMaxHalf = Math.max(thisSize.width, thisSize.depth) / 2;
|
||||
const baseDistance = thisMaxHalf + this.formationSeparation;
|
||||
|
||||
// Check the distance to twin formations, and merge if
|
||||
// the formations could collide.
|
||||
for (let i = this.twinFormations.length - 1; i >= 0; --i)
|
||||
{
|
||||
// Only do the check on one side.
|
||||
if (this.twinFormations[i] <= this.entity)
|
||||
const otherFormationID = this.twinFormations[i];
|
||||
|
||||
// Skip invalid entities and self
|
||||
if (otherFormationID == INVALID_ENTITY || otherFormationID == this.entity)
|
||||
continue;
|
||||
|
||||
const otherFormationAI = Engine.QueryInterface(otherFormationID, IID_UnitAI);
|
||||
|
||||
// If both formations aren't idle, we can do the distance check on only one side,
|
||||
// so skip one of them.
|
||||
if (!otherFormationAI.isIdle && otherFormationID <= this.entity)
|
||||
continue;
|
||||
|
||||
const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
const cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position);
|
||||
const cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation);
|
||||
const cmpOtherPosition = Engine.QueryInterface(otherFormationID, IID_Position);
|
||||
const cmpOtherFormation = Engine.QueryInterface(otherFormationID, IID_Formation);
|
||||
|
||||
if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation ||
|
||||
!cmpPosition.IsInWorld() || !cmpOtherPosition.IsInWorld())
|
||||
continue;
|
||||
@@ -953,36 +975,27 @@ Formation.prototype.ShapeUpdate = function()
|
||||
const thisPosition = cmpPosition.GetPosition2D();
|
||||
const otherPosition = cmpOtherPosition.GetPosition2D();
|
||||
|
||||
const dx = thisPosition.x - otherPosition.x;
|
||||
const dy = thisPosition.y - otherPosition.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const dist = thisPosition.distanceTo(otherPosition);
|
||||
|
||||
const thisSize = this.GetSize();
|
||||
const otherSize = cmpOtherFormation.GetSize();
|
||||
const minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) +
|
||||
Math.max(otherSize.width / 2, otherSize.depth / 2) +
|
||||
this.formationSeparation;
|
||||
const minDist = baseDistance + Math.max(otherSize.width, otherSize.depth) / 2;
|
||||
|
||||
if (minDist < dist)
|
||||
continue;
|
||||
|
||||
// Merge the members from the twin formation into this one
|
||||
// twin formations should always have exactly the same orders.
|
||||
// Merge the members from the other formation into this one
|
||||
const otherMembers = cmpOtherFormation.members;
|
||||
cmpOtherFormation.RemoveMembers(otherMembers);
|
||||
this.AddMembers(otherMembers);
|
||||
}
|
||||
// Switch between column and box if necessary.
|
||||
const cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
|
||||
const walkingDistance = cmpUnitAI.ComputeWalkingDistance();
|
||||
const columnar = walkingDistance > g_ColumnDistanceThreshold;
|
||||
if (columnar != this.columnar)
|
||||
{
|
||||
this.offsets = undefined;
|
||||
this.columnar = columnar;
|
||||
// Disable moveCenter so we can't get stuck in a loop of switching
|
||||
// shape causing center to change causing shape to switch back.
|
||||
this.MoveMembersIntoFormation(false, true, this.lastOrderVariant);
|
||||
|
||||
// We add members from the other formation to this one.
|
||||
// The other formation will get disbanded for having no members
|
||||
this.AddMembers(otherMembers, true);
|
||||
|
||||
// Remove the merged formation from twin formations list
|
||||
this.twinFormations.splice(i, 1);
|
||||
|
||||
// Update Formation and members position
|
||||
this.UpdateFormation(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1009,16 +1022,14 @@ Formation.prototype.OnGlobalEntityRenamed = function(msg)
|
||||
if (this.finishedEntities.delete(msg.entity))
|
||||
this.finishedEntities.add(msg.newentity);
|
||||
|
||||
// Save rearranging to temporarily set it to false.
|
||||
const temp = this.rearrange;
|
||||
this.rearrange = false;
|
||||
|
||||
// First remove the old member to be able to reuse its position.
|
||||
this.RemoveMembers([msg.entity], true);
|
||||
this.AddMembers([msg.newentity]);
|
||||
this.memberPositions[msg.newentity] = this.memberPositions[msg.entity];
|
||||
|
||||
this.rearrange = temp;
|
||||
// Update Formation
|
||||
// to make sure added (renamed) members will move with the controller if applicable.
|
||||
this.UpdateFormation(false, false);
|
||||
};
|
||||
|
||||
Formation.prototype.RegisterTwinFormation = function(entity)
|
||||
@@ -1047,6 +1058,98 @@ Formation.prototype.LoadFormation = function(newTemplate)
|
||||
return Engine.QueryInterface(newFormation, IID_UnitAI);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates formation members' positions based on current state.
|
||||
* Moves members into appropriate formation type if rearrangement is allowed.
|
||||
*
|
||||
* @param {boolean} moveCenter - Whether to move the formation center
|
||||
* @param {boolean} force - Force rearrangement regardless of state
|
||||
* @param {string} formationType - Formation variant to be passed as order parameter.
|
||||
*/
|
||||
Formation.prototype.UpdateFormation = function(moveCenter = false, force = false, formationType = "default") {
|
||||
// Move members into appropriate formation type
|
||||
if (this.IsRearrangementAllowed() || force)
|
||||
this.ArrangeFormation(moveCenter, force, formationType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the formation should rearrange based on controller and member states.
|
||||
* Prevents rearrangement during critical combat or activity states.
|
||||
*
|
||||
* @returns {boolean} True if formation should rearrange, false otherwise
|
||||
*/
|
||||
Formation.prototype.IsRearrangementAllowed = function()
|
||||
{
|
||||
if (this.IsControllerBlockingRearrangement())
|
||||
return false;
|
||||
|
||||
return !this.AreMembersBlockingRearrangement();
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the formation controller is in a state that prevents rearrangement.
|
||||
*
|
||||
* @returns {boolean} True if controller is in a no-rearrange state
|
||||
*/
|
||||
Formation.prototype.IsControllerBlockingRearrangement = function()
|
||||
{
|
||||
const cmpControllerAI = Engine.QueryInterface(this.entity, IID_UnitAI);
|
||||
|
||||
const noRearrangeStates = [
|
||||
"COMBAT.ATTACKING"
|
||||
];
|
||||
const state = cmpControllerAI.GetCurrentState();
|
||||
return noRearrangeStates.some(noState => state.includes(noState));
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if any formation members are in critical states that shouldn't be interrupted.
|
||||
* Uses a threshold to allow rearrangement when only a small percentage are busy.
|
||||
*
|
||||
* @returns {boolean} True if significant number of members are in critical states
|
||||
*/
|
||||
Formation.prototype.AreMembersBlockingRearrangement = function()
|
||||
{
|
||||
const criticalStates = new Set([
|
||||
"COMBAT.ATTACKING",
|
||||
"COMBAT.CHASING",
|
||||
"COMBAT.APPROACHING",
|
||||
"HEAL.HEALING",
|
||||
"GATHER",
|
||||
"REPAIR"
|
||||
]);
|
||||
|
||||
let totalMembers = 0;
|
||||
let criticalMembers = 0;
|
||||
|
||||
for (const member of this.members)
|
||||
{
|
||||
const cmpMemberAI = Engine.QueryInterface(member, IID_UnitAI);
|
||||
if (!cmpMemberAI)
|
||||
continue;
|
||||
|
||||
totalMembers++;
|
||||
const state = cmpMemberAI.GetCurrentState();
|
||||
|
||||
for (const criticalState of criticalStates)
|
||||
{
|
||||
if (state.includes(criticalState))
|
||||
{
|
||||
criticalMembers++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid members, return false
|
||||
if (totalMembers === 0)
|
||||
return false;
|
||||
|
||||
// Return true if more than 5% are in critical states.
|
||||
// Cover edge cases where a few members would be stuck
|
||||
// somewhere and still fighting for example.
|
||||
return (criticalMembers / totalMembers) > 0.05;
|
||||
};
|
||||
|
||||
Formation.prototype.OnEntityRenamed = function(msg)
|
||||
{
|
||||
|
||||
@@ -297,13 +297,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
|
||||
if (cmpFormation)
|
||||
{
|
||||
cmpFormation.SetRearrange(false);
|
||||
// Triggers FormationLeave, which ultimately will FinishOrder,
|
||||
// discarding this order.
|
||||
cmpFormation.RemoveMembers([this.entity]);
|
||||
cmpFormation.SetRearrange(true);
|
||||
}
|
||||
return ACCEPT_ORDER;
|
||||
},
|
||||
|
||||
@@ -772,12 +766,13 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
},
|
||||
|
||||
"Order.Attack": function(msg) {
|
||||
let target = msg.data.target;
|
||||
const target = msg.data.target;
|
||||
let formationTarget;
|
||||
const cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
|
||||
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
|
||||
target = cmpTargetUnitAI.GetFormationController();
|
||||
formationTarget = cmpTargetUnitAI.GetFormationController();
|
||||
|
||||
if (!this.CheckFormationTargetAttackRange(target))
|
||||
if (!this.CheckFormationTargetAttackRange(formationTarget || target))
|
||||
{
|
||||
if (this.AbleToMove() && this.CheckTargetVisible(target))
|
||||
{
|
||||
@@ -786,7 +781,8 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
}
|
||||
return this.FinishOrder();
|
||||
}
|
||||
this.CallMemberFunction("Attack", [target, msg.data.allowCapture, false]);
|
||||
this.CallMemberFunction("Attack", [formationTarget || target, msg.data.allowCapture, false]);
|
||||
|
||||
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (cmpAttack && cmpAttack.CanAttackAsFormation())
|
||||
this.SetNextState("COMBAT.ATTACKING");
|
||||
@@ -997,12 +993,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
"IDLE": {
|
||||
"enter": function(msg) {
|
||||
// Turn rearrange off. Otherwise, if the formation is idle
|
||||
// but individual units go off to fight,
|
||||
// any death will rearrange the formation, which looks odd.
|
||||
// Instead, move idle units in formation on a timer.
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
cmpFormation.SetRearrange(false);
|
||||
// Start the timer on the next turn to catch up with potential stragglers.
|
||||
this.StartTimer(100, 2000);
|
||||
this.isIdle = true;
|
||||
@@ -1020,17 +1010,16 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
if (!cmpFormation)
|
||||
return;
|
||||
|
||||
if (this.TestAllMemberFunction("IsIdle"))
|
||||
cmpFormation.MoveMembersIntoFormation(false, false);
|
||||
this.RequestFormationUpdate();
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
"WALKING": {
|
||||
"enter": function() {
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
cmpFormation.SetRearrange(true);
|
||||
cmpFormation.MoveMembersIntoFormation(true, true);
|
||||
this.RequestFormationUpdate(true, true);
|
||||
this.StartTimer(100, 2000);
|
||||
|
||||
if (!this.MoveTo(this.order.data))
|
||||
{
|
||||
this.FinishOrder();
|
||||
@@ -1045,32 +1034,20 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
},
|
||||
|
||||
"MovementUpdate": function(msg) {
|
||||
if (msg.veryObstructed && !this.timer)
|
||||
{
|
||||
// It's possible that the controller (with large clearance)
|
||||
// is stuck, but not the individual units.
|
||||
// Ask them to move individually for a little while.
|
||||
this.CallMemberFunction("MoveTo", [this.order.data]);
|
||||
this.StartTimer(3000);
|
||||
return;
|
||||
}
|
||||
else if (this.timer)
|
||||
return;
|
||||
if (msg.likelyFailure || this.CheckRange(this.order.data))
|
||||
if (msg.veryObstructed && !this.obstructionMitigationAttempted)
|
||||
this.AttemptObstructionMitigation();
|
||||
else if (msg.likelyFailure || this.CheckRange(this.order.data))
|
||||
this.FinishOrder();
|
||||
},
|
||||
|
||||
"Timer": function() {
|
||||
// Reenter to reset the pathfinder state.
|
||||
this.SetNextState("WALKING");
|
||||
// Update Formation in case some members left.
|
||||
this.RequestFormationUpdate(false, true);
|
||||
}
|
||||
},
|
||||
|
||||
"WALKINGANDFIGHTING": {
|
||||
"enter": function(msg) {
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
cmpFormation.SetRearrange(true);
|
||||
cmpFormation.MoveMembersIntoFormation(true, true, "combat");
|
||||
this.RequestFormationUpdate(true, true, "combat");
|
||||
if (!this.MoveTo(this.order.data))
|
||||
{
|
||||
this.FinishOrder();
|
||||
@@ -1095,7 +1072,9 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
},
|
||||
|
||||
"MovementUpdate": function(msg) {
|
||||
if (msg.likelyFailure || this.CheckRange(this.order.data))
|
||||
if (msg.veryObstructed && !this.obstructionMitigationAttempted)
|
||||
this.AttemptObstructionMitigation();
|
||||
else if (msg.likelyFailure || this.CheckRange(this.order.data))
|
||||
this.FinishOrder();
|
||||
},
|
||||
},
|
||||
@@ -1128,10 +1107,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
"PATROLLING": {
|
||||
"enter": function() {
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
cmpFormation.SetRearrange(true);
|
||||
cmpFormation.MoveMembersIntoFormation(true, true, "combat");
|
||||
|
||||
const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
if (!cmpPosition || !cmpPosition.IsInWorld() ||
|
||||
!this.MoveTo(this.order.data))
|
||||
@@ -1151,11 +1126,22 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
},
|
||||
|
||||
"Timer": function(msg) {
|
||||
// Force the units to stick together
|
||||
// TODO : better handling of formation patrolling
|
||||
// See https://gitea.wildfiregames.com/0ad/0ad/issues/8558
|
||||
this.RequestFormationUpdate(true, true, "combat");
|
||||
|
||||
if (this.FindWalkAndFightTargets())
|
||||
this.SetNextState("MEMBER");
|
||||
},
|
||||
|
||||
"MovementUpdate": function(msg) {
|
||||
if (msg.veryObstructed && !this.obstructionMitigationAttempted)
|
||||
{
|
||||
this.AttemptObstructionMitigation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange))
|
||||
return;
|
||||
|
||||
@@ -1202,10 +1188,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
this.FinishOrder();
|
||||
return true;
|
||||
}
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
cmpFormation.SetRearrange(true);
|
||||
cmpFormation.MoveMembersIntoFormation(true, true);
|
||||
this.RequestFormationUpdate(true, true);
|
||||
|
||||
// If the holder should pickup, warn it so it can take needed action.
|
||||
const cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder);
|
||||
@@ -1245,9 +1228,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
"FORMING": {
|
||||
"enter": function() {
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
cmpFormation.SetRearrange(true);
|
||||
cmpFormation.MoveMembersIntoFormation(true, true);
|
||||
this.RequestFormationUpdate(true, true);
|
||||
|
||||
if (!this.MoveTo(this.order.data))
|
||||
{
|
||||
@@ -1272,9 +1253,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
"COMBAT": {
|
||||
"APPROACHING": {
|
||||
"enter": function() {
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
cmpFormation.SetRearrange(true);
|
||||
cmpFormation.MoveMembersIntoFormation(true, true, "combat");
|
||||
this.RequestFormationUpdate(true, true, "combat");
|
||||
|
||||
if (!this.MoveFormationToTargetAttackRange(this.order.data.target))
|
||||
{
|
||||
@@ -1316,11 +1295,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
this.FinishOrder();
|
||||
return true;
|
||||
}
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
// TODO fix the rearranging while attacking as formation
|
||||
cmpFormation.SetRearrange(!this.IsAttackingAsFormation());
|
||||
cmpFormation.MoveMembersIntoFormation(false, false, "combat");
|
||||
this.RequestFormationUpdate(false, false, "combat");
|
||||
this.StartTimer(200, 200);
|
||||
return false;
|
||||
},
|
||||
@@ -1341,9 +1316,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
"leave": function(msg) {
|
||||
this.StopTimer();
|
||||
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
if (cmpFormation)
|
||||
cmpFormation.SetRearrange(true);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1362,11 +1334,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
},
|
||||
|
||||
"enter": function(msg) {
|
||||
// Don't rearrange the formation, as that forces all units to stop
|
||||
// what they're doing.
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
if (cmpFormation)
|
||||
cmpFormation.SetRearrange(false);
|
||||
// While waiting on members, the formation is more like
|
||||
// a group of unit and does not have a well-defined position,
|
||||
// so move the controller out of the world to enforce that.
|
||||
@@ -1391,10 +1358,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
"leave": function(msg) {
|
||||
this.StopTimer();
|
||||
// Reform entirely as members might be all over the place now.
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
if (cmpFormation && (cmpFormation.AreAllMembersIdle() || this.orderQueue.length))
|
||||
cmpFormation.MoveMembersIntoFormation(true);
|
||||
|
||||
// Update the held position so entities respond to orders.
|
||||
const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
@@ -1411,6 +1374,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
// States for entities moving as part of a formation:
|
||||
"FORMATIONMEMBER": {
|
||||
"FormationLeave": function(msg) {
|
||||
// Called when selecting units out of a formation
|
||||
// Stop moving as soon as the formation disbands
|
||||
// Keep current rotation
|
||||
const facePointAfterMove = this.GetFacePointAfterMove();
|
||||
@@ -1611,7 +1575,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
const cmpFormationAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
|
||||
if (!cmpFormationAI || !cmpFormationAI.IsIdle())
|
||||
return;
|
||||
Engine.QueryInterface(this.formationController, IID_Formation).SetIdleEntity(this.entity);
|
||||
}
|
||||
|
||||
this.isIdle = true;
|
||||
@@ -1651,8 +1614,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
if (this.isIdle)
|
||||
{
|
||||
if (this.IsFormationMember())
|
||||
Engine.QueryInterface(this.formationController, IID_Formation).UnsetIdleEntity(this.entity);
|
||||
this.isIdle = false;
|
||||
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
|
||||
}
|
||||
@@ -1721,7 +1682,6 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
const cmpFormationAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
|
||||
if (!cmpFormationAI || !cmpFormationAI.IsIdle())
|
||||
return;
|
||||
Engine.QueryInterface(this.formationController, IID_Formation).SetIdleEntity(this.entity);
|
||||
}
|
||||
|
||||
this.isIdle = true;
|
||||
@@ -2079,6 +2039,11 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
"APPROACHING": {
|
||||
"enter": function() {
|
||||
// Set this.order.data.target and this.order.formationTarget.
|
||||
// Should only mutate these once, when receiving a order with
|
||||
// a formation controller as target.
|
||||
this.HandleTargetAsFormation();
|
||||
|
||||
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
{
|
||||
this.FinishOrder();
|
||||
@@ -2159,14 +2124,10 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
|
||||
"ATTACKING": {
|
||||
"enter": function() {
|
||||
let target = this.order.data.target;
|
||||
const cmpFormation = Engine.QueryInterface(target, IID_Formation);
|
||||
if (cmpFormation)
|
||||
{
|
||||
this.order.data.formationTarget = target;
|
||||
target = cmpFormation.GetClosestMember(this.entity);
|
||||
this.order.data.target = target;
|
||||
}
|
||||
// Set this.order.data.target and this.order.formationTarget.
|
||||
// Should only mutate these once, when receiving a order with
|
||||
// a formation controller as target.
|
||||
this.HandleTargetAsFormation();
|
||||
|
||||
this.shouldCheer = false;
|
||||
|
||||
@@ -2177,7 +2138,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
|
||||
if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
|
||||
{
|
||||
if (this.CanPack())
|
||||
{
|
||||
@@ -2263,18 +2224,25 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
},
|
||||
|
||||
"enter": function() {
|
||||
// Try to find the formation the target was a part of.
|
||||
// Check if we are attacking a formation.
|
||||
let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
|
||||
if (!cmpFormation)
|
||||
if (cmpFormation)
|
||||
this.order.data.formationTarget = this.order.data.target;
|
||||
else
|
||||
cmpFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
|
||||
|
||||
// If the target is a formation, pick closest member.
|
||||
if (cmpFormation)
|
||||
{
|
||||
const filter = (t) => this.CanAttack(t);
|
||||
this.order.data.formationTarget = this.order.data.target;
|
||||
const target = cmpFormation.GetClosestMember(this.entity, filter);
|
||||
this.order.data.target = target;
|
||||
// As we're also setting this.order.data.target, if GetClosestMemberToEntity returns INVALID_ENTITY, this should prevent us from reentering
|
||||
// here again right away, as cmpFormation would be null.
|
||||
// Now we cannot have an infinite loop if ATTACKING send us to FINDINGNEWTARGET again.
|
||||
this.order.data.target = cmpFormation.GetClosestMemberToEntity(this.entity, filter);
|
||||
|
||||
if (this.order.data.target == INVALID_ENTITY)
|
||||
delete this.order.data.formationTarget;
|
||||
|
||||
this.SetNextState("COMBAT.ATTACKING");
|
||||
return true;
|
||||
}
|
||||
@@ -3315,12 +3283,7 @@ UnitAI.prototype.UnitFsmSpec = {
|
||||
{
|
||||
const cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
|
||||
if (cmpFormation)
|
||||
{
|
||||
const rearrange = cmpFormation.rearrange;
|
||||
cmpFormation.SetRearrange(false);
|
||||
cmpFormation.RemoveMembers([this.entity]);
|
||||
cmpFormation.SetRearrange(rearrange);
|
||||
}
|
||||
}
|
||||
|
||||
const cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
|
||||
@@ -4771,7 +4734,7 @@ UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(target, IID_Formation);
|
||||
if (cmpFormation)
|
||||
target = cmpFormation.GetClosestMember(this.entity);
|
||||
target = cmpFormation.GetClosestMemberToEntity(this.entity);
|
||||
|
||||
if (type != "Ranged")
|
||||
return this.MoveToTargetRange(target, IID_Attack, type);
|
||||
@@ -4812,7 +4775,7 @@ UnitAI.prototype.MoveFormationToTargetAttackRange = function(target)
|
||||
{
|
||||
const cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
|
||||
if (cmpTargetFormation)
|
||||
target = cmpTargetFormation.GetClosestMember(this.entity);
|
||||
target = cmpTargetFormation.GetClosestMemberToEntity(this.entity);
|
||||
|
||||
if (!this.CheckTargetVisible(target))
|
||||
return false;
|
||||
@@ -4826,6 +4789,18 @@ UnitAI.prototype.MoveFormationToTargetAttackRange = function(target)
|
||||
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests the formation component to update member positions.
|
||||
* Called by formation controller when formation needs reorganization.
|
||||
*/
|
||||
UnitAI.prototype.RequestFormationUpdate = function(moveCenter = false, force = false, formationType = "default") {
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
if (!cmpFormation)
|
||||
return;
|
||||
|
||||
cmpFormation.UpdateFormation(moveCenter, force, formationType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic dispatcher for other Check...Range functions.
|
||||
* @param iid - Interface ID (optional) implementing GetRange
|
||||
@@ -4883,7 +4858,7 @@ UnitAI.prototype.CheckTargetAttackRange = function(target, type)
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(target, IID_Formation);
|
||||
if (cmpFormation)
|
||||
target = cmpFormation.GetClosestMember(this.entity);
|
||||
target = cmpFormation.GetClosestMemberToEntity(this.entity);
|
||||
|
||||
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
return cmpAttack && cmpAttack.IsTargetInRange(target, type);
|
||||
@@ -4905,7 +4880,7 @@ UnitAI.prototype.CheckFormationTargetAttackRange = function(target)
|
||||
{
|
||||
const cmpTargetFormation = Engine.QueryInterface(target, IID_Formation);
|
||||
if (cmpTargetFormation)
|
||||
target = cmpTargetFormation.GetClosestMember(this.entity);
|
||||
target = cmpTargetFormation.GetClosestMemberToEntity(this.entity);
|
||||
|
||||
const cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpFormationAttack)
|
||||
@@ -5202,6 +5177,23 @@ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
|
||||
|
||||
// External interface functions
|
||||
|
||||
/**
|
||||
* Checks if the current target is a formation controller.
|
||||
* If it is a formation controller, we identify the closest individual
|
||||
* member within it and set this.order.data.target to it instead.
|
||||
* The original formation controller ID is stored in this.order.data.formationTarget
|
||||
* instead.
|
||||
*/
|
||||
UnitAI.prototype.HandleTargetAsFormation = function()
|
||||
{
|
||||
const cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation);
|
||||
if (!cmpFormation)
|
||||
return;
|
||||
|
||||
this.order.data.formationTarget = this.order.data.target;
|
||||
this.order.data.target = cmpFormation.GetClosestMemberToEntity(this.entity);
|
||||
};
|
||||
|
||||
/**
|
||||
* Order a unit to leave the formation it is in.
|
||||
* Used to handle queued no-formation orders for units in formation.
|
||||
@@ -6565,16 +6557,67 @@ UnitAI.prototype.CallPlayerOwnedEntitiesFunctionInRange = function(funcname, arg
|
||||
};
|
||||
|
||||
/**
|
||||
* Call obj.functname(args) on UnitAI components of all formation members,
|
||||
* and return true if all calls return true.
|
||||
* Attempts to mitigate formation obstruction by teleporting the formation controller
|
||||
* to the member closest to the destination.
|
||||
*
|
||||
* This is used when the formation controller (with large clearance) is stuck on obstacles,
|
||||
* but individual units can still navigate. The controller jumps to a member's position
|
||||
* to bypass the obstruction.
|
||||
*
|
||||
* The mitigation have a cooldown to prevent any weird behaviour.
|
||||
*/
|
||||
UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
|
||||
UnitAI.prototype.AttemptObstructionMitigation = function()
|
||||
{
|
||||
if (this.obstructionMitigationAttempted)
|
||||
return;
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
|
||||
return cmpFormation && cmpFormation.GetMembers().every(ent => {
|
||||
const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
return cmpUnitAI[funcname].apply(cmpUnitAI, args);
|
||||
});
|
||||
const destination = this.order.data;
|
||||
|
||||
if (!cmpFormation || !destination || destination.x === undefined || destination.z === undefined)
|
||||
return;
|
||||
|
||||
// Convert x,z coordinates to x,y coordinates for GetClosestMemberToPosition
|
||||
const destinationVec = new Vector2D(destination.x, destination.z);
|
||||
|
||||
const closestMember = cmpFormation.GetClosestMemberToPosition(destinationVec);
|
||||
if (closestMember == INVALID_ENTITY)
|
||||
return;
|
||||
|
||||
const cmpMemberPosition = Engine.QueryInterface(closestMember, IID_Position);
|
||||
const cmpControllerPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
if (!cmpMemberPosition || !cmpControllerPosition)
|
||||
return;
|
||||
|
||||
const memberPosObj = cmpMemberPosition.GetPosition2D();
|
||||
const controllerPosObj = cmpControllerPosition.GetPosition2D();
|
||||
|
||||
const memberPos = new Vector2D(memberPosObj.x, memberPosObj.y);
|
||||
const controllerPos = new Vector2D(controllerPosObj.x, controllerPosObj.y);
|
||||
|
||||
// Check if member is more than 2 meters closer to destination than controller
|
||||
if (memberPos.distanceTo(destinationVec) < controllerPos.distanceTo(destinationVec) - 2)
|
||||
cmpControllerPosition.JumpTo(memberPos.x, memberPos.y);
|
||||
|
||||
this.SetObstructionMitigationFlag();
|
||||
};
|
||||
UnitAI.prototype.SetObstructionMitigationFlag = function()
|
||||
{
|
||||
this.obstructionMitigationAttempted = true;
|
||||
|
||||
// Clear existing timer if any
|
||||
if (this.obstructionMitigationTimer !== undefined)
|
||||
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
|
||||
.CancelTimer(this.obstructionMitigationTimer);
|
||||
|
||||
// Store the new timer ID
|
||||
this.obstructionMitigationTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
|
||||
.SetTimeout(this.entity, IID_UnitAI, "ResetObstructionMitigationFlag", 5000, {});
|
||||
};
|
||||
|
||||
UnitAI.prototype.ResetObstructionMitigationFlag = function()
|
||||
{
|
||||
delete this.obstructionMitigationAttempted;
|
||||
};
|
||||
|
||||
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
|
||||
|
||||
Reference in New Issue
Block a user