0ad/binaries/data/mods/public/simulation/components/RallyPoint.js
mehmed-faheim-arslan 39b1311fac Allow players to set rally points on allied buildings
Instead of storing a single flat list of positions and data per
building, RallyPoint now stores them keyed by player ID. This lets
mutual allies independently set and display rally points on each
other's structures.

The GUI now allows selecting allied buildings with a rally point
and only shows the viewing player's own rally point data.
GuiInterface gets an OnUpdate handler to keep displayed positions
in sync when rally point targets move.

GetRallyPointCommands now takes raw position and data arrays instead
of a component reference. The network command field is also renamed
from "entities" to "structures".

Fixes #3115
2026-06-10 00:09:48 +02:00

171 lines
5 KiB
JavaScript

function RallyPoint() {}
RallyPoint.prototype.Schema =
"<a:component/><empty/>";
RallyPoint.prototype.Init = function()
{
this.perPlayer = {};
};
RallyPoint.prototype.GetOwner = function()
{
return Engine.QueryInterface(this.entity, IID_Ownership)?.GetOwner();
};
RallyPoint.prototype.AddPosition = function(x, z, player = this.GetOwner())
{
if (!this.perPlayer[player])
this.perPlayer[player] = { "pos": [], "data": [] };
this.perPlayer[player].pos.push({ "x": x, "z": z });
};
RallyPoint.prototype.HasPositions = function(player = this.GetOwner())
{
return !!this.perPlayer[player]?.pos.length;
};
RallyPoint.prototype.GetFirstPosition = function()
{
const pos = this.perPlayer[this.GetOwner()]?.pos;
return pos?.length ? Vector2D.from3D(pos[0]) : new Vector2D(-1, -1);
};
RallyPoint.prototype.GetPositions = function(player = this.GetOwner())
{
const playerEntry = this.perPlayer[player];
if (!playerEntry)
return [];
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// We must not affect the simulation state here, so copy the state
const ret = [];
for (let i = 0; i < playerEntry.pos.length; i++)
{
ret.push(playerEntry.pos[i]);
// Update the rallypoint coordinates if the target is alive
if (!playerEntry.data[i]?.target || !this.TargetIsAlive(playerEntry.data[i].target))
continue;
// and visible to the player who set this rally point
if (cmpRangeManager &&
cmpRangeManager.GetLosVisibility(playerEntry.data[i].target, player) != "visible")
continue;
// Get the actual position of the target entity
const cmpPosition = Engine.QueryInterface(playerEntry.data[i].target, IID_Position);
if (!cmpPosition?.IsInWorld())
continue;
const targetPosition = cmpPosition.GetPosition2D();
if (!targetPosition)
continue;
if (playerEntry.pos[i].x == targetPosition.x && playerEntry.pos[i].z == targetPosition.y)
continue;
ret[i] = { "x": targetPosition.x, "z": targetPosition.y };
}
return ret;
};
// Extra data for the rally point, should have a command property and then helpful data for that command
// See getActionInfo in gui/input.js
RallyPoint.prototype.AddData = function(data, player = this.GetOwner())
{
if (!this.perPlayer[player])
this.perPlayer[player] = { "pos": [], "data": [] };
this.perPlayer[player].data.push(data);
};
// Returns an array with the data associated with this rally point. Each element has the structure:
// {"type": "walk/gather/garrison/...", "target": targetEntityId, "resourceType": "tree/fruit/ore/..."} where target
// and resourceType (specific resource type) are optional, also target may be an invalid entity, check for existence.
RallyPoint.prototype.GetData = function(player = this.GetOwner())
{
return this.perPlayer[player]?.data ?? [];
};
RallyPoint.prototype.Unset = function(player = this.GetOwner())
{
delete this.perPlayer[player];
};
/**
* @param {number} entity - The entity ID of the entity to order to the rally point.
* @param {string[]} ignore - The commands to ignore when performed on this.entity.
* E.g. "garrison" when unloading.
*/
RallyPoint.prototype.OrderToRallyPoint = function(entity, ignore = [])
{
const cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (!cmpEntOwnership)
return;
const entOwner = cmpEntOwnership.GetOwner();
if (!this.HasPositions(entOwner))
return;
const playerEntry = this.perPlayer[entOwner];
const commands = GetRallyPointCommands(playerEntry.pos, playerEntry.data, [entity]);
if (!commands.length ||
commands[0].target == this.entity && ignore.includes(commands[0].type))
return;
for (const command of commands)
ProcessCommand(entOwner, command);
};
RallyPoint.prototype.OnGlobalEntityRenamed = function(msg)
{
for (const playerEntry of Object.values(this.perPlayer))
for (const data of playerEntry.data)
{
if (data?.target == msg.entity)
data.target = msg.newentity;
if (data?.source == msg.entity)
data.source = msg.newentity;
}
if (msg.entity != this.entity)
return;
const cmpRallyPointNew = Engine.QueryInterface(msg.newentity, IID_RallyPoint);
if (cmpRallyPointNew)
for (const player in this.perPlayer)
{
const playerEntry = this.perPlayer[player];
for (let i = 0; i < playerEntry.pos.length; ++i)
{
cmpRallyPointNew.AddPosition(playerEntry.pos[i].x, playerEntry.pos[i].z, +player);
cmpRallyPointNew.AddData(playerEntry.data[i], +player);
}
}
};
RallyPoint.prototype.OnOwnershipChanged = function(msg)
{
// No need to reset when constructing or destructing the entity
if (msg.from == INVALID_PLAYER || msg.to == INVALID_PLAYER)
return;
this.perPlayer = {};
};
/**
* Returns true if the target exists and has non-zero hitpoints.
*/
RallyPoint.prototype.TargetIsAlive = function(ent)
{
var cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
return true;
var cmpHealth = QueryMiragedInterface(ent, IID_Health);
return cmpHealth && cmpHealth.GetHitpoints() != 0;
};
Engine.RegisterComponentType(IID_RallyPoint, "RallyPoint", RallyPoint);