1
0
forked from 0ad/0ad

Allow picking a default formation for walk (and walk-like) orders.

This allows choosing a "default formation", which is activated
automatically for units given walk orders (and attack-walk etc.).
Conversely, units in formation that are given a gather/build/... order
are taken out of formation and given the order individually.
The default formation can be selected by right-clicking on any formation
icon.

This leverages formations for walking, where they are quite efficient
(in fact, perhaps too efficient), while circumventing issues with
various orders.

Choosing the "null formation" as the default formation de-activates the
behaviour entirely, and plays out exactly like SVN.

This makes it possible to queue a formation-order then a
noformation-order (i.e. walk then repair), though the behaviour isn't
very flexible.

For modders, it should be relatively easy to change the setup for each
order, and/or to force deactivating/activating formations in general.

Tested by: Freagarach, Angen
Refs #3479, #1791.
Makes #3478 mostly invalid.

Differential Revision: https://code.wildfiregames.com/D2764
This was SVN commit r24480.
This commit is contained in:
wraitii 2020-12-31 10:04:58 +00:00
parent 83a1f93828
commit 59d0885d68
12 changed files with 257 additions and 118 deletions

View File

@ -393,6 +393,7 @@ respoptooltipsort = 0 ; Sorting players in the resources and populat
snaptoedges = "disabled" ; Possible values: disabled, enabled.
snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges
disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time.
defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already.
[gui.session.minimap]
blinkduration = 1.7 ; The blink duration while pinging

View File

@ -0,0 +1,4 @@
/**
* The 'null' formation means that units are individuals.
*/
const NULL_FORMATION = "special/formations/null";

View File

@ -0,0 +1,59 @@
/**
* Handles the logic related to the 'default formation' feature.
* When given a walking order, units that aren't in formation will be put
* in the default formation, to improve pathfinding and reactivity.
* However, when given other tasks (such as e.g. gather), they will be removed
* from any formation they are in, as those orders don't work very well with formations.
*
* Set the default formation to the null formation to disable this entirely.
*
* TODO: it would be nice to let players choose default formations for different orders,
* but that would be neater if orders where defined somewhere unique,
* instead of mostly in unit_actions.js
*/
class AutoFormation
{
constructor()
{
this.defaultFormation = Engine.ConfigDB_GetValue("user", "gui.session.defaultformation");
if (!this.defaultFormation)
this.setDefault(NULL_FORMATION);
}
/**
* Set the default formation to @param formation.
* TODO: would be good to validate, particularly since some formations aren't
* usable with any arbitrary unit type, we may want to warn then.
*/
setDefault(formation)
{
this.defaultFormation = formation;
Engine.ConfigDB_CreateValue("user", "gui.session.defaultformation", this.defaultFormation);
// TODO: It's extremely terrible that we have to explicitly flush the config...
Engine.ConfigDB_SetChanges("user", true);
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
return true;
}
isDefault(formation)
{
return formation == this.defaultFormation;
}
/**
* @return the default formation, or "undefined" if the null formation was chosen,
* otherwise units in formation would disband on any order, which isn't desirable.
*/
getDefault()
{
return this.defaultFormation == NULL_FORMATION ? undefined : this.defaultFormation;
}
/**
* @return the null formation, or "undefined" if the null formation is the default.
*/
getNull()
{
return this.defaultFormation == NULL_FORMATION ? undefined : NULL_FORMATION;
}
}

View File

@ -315,7 +315,8 @@ function tryPlaceBuilding(queued)
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] });
@ -358,6 +359,7 @@ function tryPlaceWall(queued)
"pieces": wallPlacementInfo.pieces,
"startSnappedEntity": wallPlacementInfo.startSnappedEnt,
"endSnappedEntity": wallPlacementInfo.endSnappedEnt,
"formation": g_AutoFormation.getNull()
};
// make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end
@ -1241,7 +1243,8 @@ function positionUnitsFreehandSelectionMouseUp(ev)
"entities": selection,
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
"queued": Engine.HotkeyIsPressed("session.queue")
"queued": Engine.HotkeyIsPressed("session.queue"),
"formation": NULL_FORMATION,
});
// Add target markers with a minimum distance of 5 to each other.

View File

@ -310,7 +310,7 @@ g_SelectionPanels.Formation = {
if (!g_FormationsInfo.has(data.item))
g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
let formationOk = data.item == "special/formations/null" || canMoveSelectionIntoFormation(data.item);
let formationOk = canMoveSelectionIntoFormation(data.item);
let unitIds = data.unitEntStates.map(state => state.id);
let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": unitIds,
@ -321,8 +321,21 @@ g_SelectionPanels.Formation = {
performFormation(unitIds, data.item);
};
data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item);
let formationInfo = g_FormationsInfo.get(data.item);
let tooltip = translate(formationInfo.name);
let isDefaultFormation = g_AutoFormation.isDefault(data.item);
if (data.item === NULL_FORMATION)
tooltip += "\n" + (isDefaultFormation ?
translate("Default formation is disabled.") :
translate("Right-click to disable the default formation feature."));
else
tooltip += "\n" + (isDefaultFormation ?
translate("This is the default formation, used for movement orders.") :
translate("Right-click to set this as the default formation."));
if (!formationOk && formationInfo.tooltip)
tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red");
data.button.tooltip = tooltip;
@ -330,6 +343,7 @@ g_SelectionPanels.Formation = {
data.button.enabled = formationOk && controlsPlayer(data.player);
let grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.countDisplay.hidden = !isDefaultFormation;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);

View File

@ -8,6 +8,8 @@ const UPGRADING_CHOSEN_OTHER = -1;
function canMoveSelectionIntoFormation(formationTemplate)
{
if (formationTemplate == NULL_FORMATION)
return true;
if (!(formationTemplate in g_canMoveIntoFormation))
g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
@ -332,7 +334,7 @@ function performFormation(entities, formationTemplate)
Engine.PostNetworkCommand({
"type": "formation",
"entities": entities,
"name": formationTemplate
"formation": formationTemplate
});
}

View File

@ -7,6 +7,7 @@
<object name="unitFormationButton[n]" hidden="true" style="iconButton" type="button" size="0 0 38 38" tooltip_style="sessionToolTipBottomBold" z="100">
<object name="unitFormationIcon[n]" type="image" ghost="true" size="3 3 35 35"/>
<object name="unitFormationSelection[n]" hidden="true" type="image" ghost="true" size="3 3 35 35" sprite="stretched:session/icons/corners.png"/>
<object name="unitFormationCount[n]" hidden="true" type="image" ghost="true" size="3 3 35 35" sprite="stretched:session/icons/upgrade.png"/>
</object>
</repeat>
</object>

View File

@ -10,6 +10,7 @@ const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.Starting
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
var g_Ambient;
var g_AutoFormation;
var g_Chat;
var g_Cheats;
var g_DeveloperOverlay;
@ -277,6 +278,7 @@ function init(initData, hotloadData)
g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates);
g_Ambient = new Ambient();
g_AutoFormation = new AutoFormation();
g_Chat = new Chat(g_PlayerViewControl, g_Cheats);
g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection);
g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors);

View File

@ -52,7 +52,8 @@ var g_UnitActions =
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
@ -102,7 +103,8 @@ var g_UnitActions =
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
@ -146,7 +148,8 @@ var g_UnitActions =
"entities": selection,
"target": action.target,
"allowCapture": true,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getDefault()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -191,7 +194,8 @@ var g_UnitActions =
"entities": selection,
"target": action.target,
"queued": queued,
"allowCapture": false
"allowCapture": false,
"formation": g_AutoFormation.getDefault()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -244,7 +248,8 @@ var g_UnitActions =
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
"queued": queued,
"allowCapture": false
"allowCapture": false,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
@ -293,7 +298,8 @@ var g_UnitActions =
"type": "heal",
"entities": selection,
"target": action.target,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -358,7 +364,8 @@ var g_UnitActions =
"entities": selection,
"target": action.target,
"autocontinue": true,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -415,7 +422,8 @@ var g_UnitActions =
"type": "gather",
"entities": selection,
"target": action.target,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -465,7 +473,8 @@ var g_UnitActions =
"type": "returnresource",
"entities": selection,
"target": action.target,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -574,7 +583,8 @@ var g_UnitActions =
"target": action.target,
"source": null,
"route": null,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -674,7 +684,8 @@ var g_UnitActions =
"type": "garrison",
"entities": selection,
"target": action.target,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
@ -746,7 +757,8 @@ var g_UnitActions =
"type": "guard",
"entities": selection,
"target": action.target,
"queued": queued
"queued": queued,
"formation": g_AutoFormation.getDefault()
});
Engine.GuiInterfaceCall("PlaySound", {

View File

@ -1037,6 +1037,8 @@ Formation.prototype.LoadFormation = function(newTemplate)
cmpNewUnitAI.MoveIntoFormation();
Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": newFormation });
return cmpNewUnitAI;
};
Engine.RegisterComponentType(IID_Formation, "Formation", Formation);

View File

@ -247,7 +247,23 @@ UnitAI.prototype.UnitFsmSpec = {
},
// Individual orders:
// (these will switch the unit out of formation mode)
"Order.LeaveFormation": function() {
if (!this.IsFormationMember())
{
this.FinishOrder();
return;
}
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
cmpFormation.SetRearrange(false);
// Calls FinishOrder();
cmpFormation.RemoveMembers([this.entity]);
cmpFormation.SetRearrange(true);
}
},
"Order.Stop": function(msg) {
this.StopMoving();
@ -5129,6 +5145,23 @@ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
//// External interface functions ////
/**
* Order a unit to leave the formation it is in.
* Used to handle queued no-formation orders for units in formation.
*/
UnitAI.prototype.LeaveFormation = function(queued = true)
{
// If queued, add the order even if we're not in formation,
// maybe we will be later.
if (!queued && !this.IsFormationMember())
return;
if (queued)
this.AddOrder("LeaveFormation", { "force": true }, queued);
else
this.PushOrderFront("LeaveFormation", { "force": true });
};
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
@ -5156,7 +5189,7 @@ UnitAI.prototype.GetFormationController = function()
UnitAI.prototype.GetFormationTemplate = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || "special/formations/null";
return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || NULL_FORMATION;
};
UnitAI.prototype.MoveIntoFormation = function(cmd)

View File

@ -16,6 +16,12 @@ function ProcessCommand(player, cmd)
if (cmd.entities)
data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
// TODO: queuing order and forcing formations doesn't really work.
// To play nice, we'll still no-formation queued order if units are in formation
// but the opposite perhaps ought to be implemented.
if (!cmd.queued || cmd.formation == NULL_FORMATION)
data.formation = cmd.formation || undefined;
// Allow focusing the camera on recent commands
let commandData = {
"type": "playercommand",
@ -137,7 +143,7 @@ var g_Commands = {
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
},
@ -145,7 +151,7 @@ var g_Commands = {
"walk-custom": function(player, cmd, data)
{
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player).forEach(cmpUnitAI => {
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued);
});
},
@ -165,7 +171,7 @@ var g_Commands = {
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued);
});
},
@ -174,7 +180,7 @@ var g_Commands = {
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player).forEach(cmpUnitAI => {
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued);
});
},
@ -187,7 +193,7 @@ var g_Commands = {
!(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued);
});
},
@ -196,7 +202,7 @@ var g_Commands = {
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI =>
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued)
);
},
@ -206,7 +212,7 @@ var g_Commands = {
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Heal(cmd.target, cmd.queued);
});
},
@ -217,7 +223,7 @@ var g_Commands = {
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
},
@ -227,14 +233,14 @@ var g_Commands = {
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
},
"gather-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued);
});
},
@ -244,7 +250,7 @@ var g_Commands = {
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
},
@ -451,7 +457,7 @@ var g_Commands = {
return;
}
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Garrison(cmd.target, cmd.queued);
});
},
@ -465,14 +471,14 @@ var g_Commands = {
return;
}
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Guard(cmd.target, cmd.queued);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Stop(cmd.queued);
});
},
@ -564,7 +570,7 @@ var g_Commands = {
"formation": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd.name).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => {
cmpUnitAI.MoveIntoFormation(cmd);
});
},
@ -617,14 +623,14 @@ var g_Commands = {
"setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
"cancel-setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CancelSetupTradeRoute(cmd.target);
});
},
@ -885,8 +891,9 @@ function notifyOrderFailure(entity, player)
*/
function ExtractFormations(ents)
{
var entities = []; // subset of ents that have UnitAI
var members = {}; // { formationentity: [ent, ent, ...], ... }
let entities = []; // subset of ents that have UnitAI
let members = {}; // { formationentity: [ent, ent, ...], ... }
let templates = {}; // { formationentity: template }
for (let ent of ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
@ -894,13 +901,16 @@ function ExtractFormations(ents)
if (fid != INVALID_ENTITY)
{
if (!members[fid])
{
members[fid] = [];
templates[fid] = cmpUnitAI.GetFormationTemplate();
}
members[fid].push(ent);
}
entities.push(ent);
}
return { "entities": entities, "members": members };
return { "entities": entities, "members": members, "templates": templates };
}
/**
@ -1156,7 +1166,8 @@ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
"queued": cmd.queued,
"formation": cmd.formation || undefined
});
}
@ -1421,14 +1432,13 @@ function RemoveFromFormation(ents)
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, formationTemplate)
function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate)
{
// If an individual was selected, remove it from any formation
// and command it individually
// and command it individually.
if (ents.length == 1)
{
// Skip unit if it has no UnitAI
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
@ -1437,101 +1447,97 @@ function GetFormationUnitAIs(ents, player, formationTemplate)
return [ cmpUnitAI ];
}
// Separate out the units that don't support the chosen formation
var formedEnts = [];
var nonformedUnitAIs = [];
for (let ent of ents)
let formationUnitAIs = [];
// Find what formations the selected entities are currently in,
// and default to that unless the formation is forced or it's the null formation
// (we want that to reset whatever formations units are in).
if (formationTemplate != NULL_FORMATION)
{
// Skip units with no UnitAI or no position
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
continue;
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
var nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == "special/formations/null";
if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "special/formations/null"))
formedEnts.push(ent);
else
{
if (nullFormation)
RemoveFromFormation([ent]);
nonformedUnitAIs.push(cmpUnitAI);
}
}
if (formedEnts.length == 0)
{
// No units support the formation - return all the others
return nonformedUnitAIs;
}
// Find what formations the formationable selected entities are currently in
var formation = ExtractFormations(formedEnts);
var formationUnitAIs = [];
let formation = ExtractFormations(ents);
let formationIds = Object.keys(formation.members);
if (formationIds.length == 1)
{
// Selected units either belong to this formation or have no formation
// Check that all its members are selected
var fid = formationIds[0];
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length
&& cmpFormation.GetMemberCount() == formation.entities.length)
// Selected units either belong to this formation or have no formation.
let fid = formationIds[0];
let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length &&
cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command
// The whole formation was selected, so reuse its controller for this command.
if (!forceTemplate)
{
formationTemplate = formation.templates[fid];
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
cmpFormation.LoadFormation(formationTemplate);
}
else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)];
}
else if (cmpFormation && !forceTemplate)
{
// Just reuse the template.
formationTemplate = formation.templates[fid];
}
}
else if (formationIds.length)
{
// Check if all entities share a common formation, if so reuse this template.
let template = formation.templates[formationIds[0]];
for (let i = 1; i < formationIds.length; ++i)
if (formation.templates[formationIds[i]] != template)
{
template = null;
break;
}
if (template && !forceTemplate)
formationTemplate = template;
}
}
// Separate out the units that don't support the chosen formation.
let formedUnits = [];
let nonformedUnitAIs = [];
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION;
if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION))
{
if (nullFormation && cmpUnitAI.GetFormationController())
cmpUnitAI.LeaveFormation(cmd.queued || false);
nonformedUnitAIs.push(cmpUnitAI);
}
else
formedUnits.push(ent);
}
if (nonformedUnitAIs.length == ents.length)
{
// No units support the formation.
return nonformedUnitAIs;
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller
// We need to give the selected units a new formation controller.
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
var formationSeparation = 60;
var clusters = ClusterEntities(formation.entities, formationSeparation);
var formationEnts = [];
let formationSeparation = 60;
let clusters = ClusterEntities(formedUnits, formationSeparation);
let formationEnts = [];
for (let cluster of clusters)
{
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
// Use the last formation template if everyone was using it
var lastFormationTemplate = undefined;
for (let ent of cluster)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
var template = cmpUnitAI.GetFormationTemplate();
if (lastFormationTemplate === undefined)
{
lastFormationTemplate = template;
}
else if (lastFormationTemplate != template)
{
lastFormationTemplate = undefined;
break;
}
}
}
if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate))
formationTemplate = lastFormationTemplate;
else
formationTemplate = "special/formations/null";
}
RemoveFromFormation(cluster);
if (formationTemplate == "special/formations/null")
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
for (let ent of cluster)
nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI));
@ -1539,9 +1545,9 @@ function GetFormationUnitAIs(ents, player, formationTemplate)
continue;
}
// Create the new controller
var formationEnt = Engine.AddEntity(formationTemplate);
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
// Create the new controller.
let formationEnt = Engine.AddEntity(formationTemplate);
let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
@ -1550,7 +1556,7 @@ function GetFormationUnitAIs(ents, player, formationTemplate)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}