Changeset View
Changeset View
Standalone View
Standalone View
binaries/data/mods/public/simulation/helpers/Commands.js
Show First 20 Lines • Show All 135 Lines • ▼ Show 20 Lines | "reveal-map": function(player, cmd, data) | ||||
// Reveal the map for all players, not just the current player, | // Reveal the map for all players, not just the current player, | ||||
// primarily to make it obvious to everyone that the player is cheating | // primarily to make it obvious to everyone that the player is cheating | ||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); | ||||
cmpRangeManager.SetLosRevealAll(-1, cmd.enable); | cmpRangeManager.SetLosRevealAll(-1, cmd.enable); | ||||
}, | }, | ||||
"walk": function(player, cmd, data) | "walk": function(player, cmd, data) | ||||
{ | { | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); | ||||
// the grouped entities will group-walk to the target, the non-grouped will just walk there. | |||||
unitAIs[0].forEach(cmpUnitAI => { | |||||
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); | cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); | ||||
}); | }); | ||||
}, | }, | ||||
"walk-to-range": function(player, cmd, data) | "walk-to-range": function(player, cmd, data) | ||||
{ | { | ||||
// Only used by the AI | // Only used by the AI | ||||
for (let ent of data.entities) | for (let ent of data.entities) | ||||
{ | { | ||||
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | ||||
if (cmpUnitAI) | if (cmpUnitAI) | ||||
cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued); | cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued); | ||||
} | } | ||||
}, | }, | ||||
"attack-walk": function(player, cmd, data) | "attack-walk": function(player, cmd, data) | ||||
{ | { | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); | ||||
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued); | for (let i in unitAIs) | ||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"attack": function(player, cmd, data) | "attack": function(player, cmd, data) | ||||
{ | { | ||||
if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) | if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) | ||||
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); | warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); | ||||
let allowCapture = cmd.allowCapture || cmd.allowCapture == null; | let allowCapture = cmd.allowCapture || cmd.allowCapture == null; | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let pos = Engine.QueryInterface(cmd.target, IID_Position); | ||||
cmpUnitAI.Attack(cmd.target, cmd.queued, allowCapture); | if (!pos) | ||||
return; | |||||
let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); | |||||
for (let i in unitAIs) | |||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.Attack(cmd.target, cmd.queued || +i, allowCapture); | |||||
}); | }); | ||||
}, | }, | ||||
"patrol": function(player, cmd, data) | "patrol": function(player, cmd, data) | ||||
{ | { | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => | let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); | ||||
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued) | for (let i in unitAIs) | ||||
); | unitAIs[i].forEach(cmpUnitAI => { | ||||
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.queued || +i) | |||||
}); | |||||
}, | }, | ||||
"heal": function(player, cmd, data) | "heal": function(player, cmd, data) | ||||
{ | { | ||||
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) | 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)); | warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let pos = Engine.QueryInterface(cmd.target, IID_Position); | ||||
cmpUnitAI.Heal(cmd.target, cmd.queued); | if (!pos) | ||||
return; | |||||
let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); | |||||
for (let i in unitAIs) | |||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.Heal(cmd.target, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"repair": function(player, cmd, data) | "repair": function(player, cmd, data) | ||||
{ | { | ||||
// This covers both repairing damaged buildings, and constructing unfinished foundations | // This covers both repairing damaged buildings, and constructing unfinished foundations | ||||
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) | if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) | ||||
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); | warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let pos = Engine.QueryInterface(cmd.target, IID_Position); | ||||
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); | if (!pos) | ||||
return; | |||||
let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); | |||||
for (let i in unitAIs) | |||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"gather": function(player, cmd, data) | "gather": function(player, cmd, data) | ||||
{ | { | ||||
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) | if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) | ||||
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); | warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let pos = Engine.QueryInterface(cmd.target, IID_Position); | ||||
cmpUnitAI.Gather(cmd.target, cmd.queued); | if (!pos) | ||||
return; | |||||
let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); | |||||
for (let i in unitAIs) | |||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.Gather(cmd.target, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"gather-near-position": function(player, cmd, data) | "gather-near-position": function(player, cmd, data) | ||||
{ | { | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, cmd.x, cmd.z, 14, cmd.queued); | ||||
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued); | for (let i in unitAIs) | ||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"returnresource": function(player, cmd, data) | "returnresource": function(player, cmd, data) | ||||
{ | { | ||||
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) | if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) | ||||
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); | warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let pos = Engine.QueryInterface(cmd.target, IID_Position); | ||||
cmpUnitAI.ReturnResource(cmd.target, cmd.queued); | if (!pos) | ||||
return; | |||||
let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); | |||||
for (let i in unitAIs) | |||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.ReturnResource(cmd.target, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"back-to-work": function(player, cmd, data) | "back-to-work": function(player, cmd, data) | ||||
{ | { | ||||
for (let ent of data.entities) | for (let ent of data.entities) | ||||
{ | { | ||||
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | ||||
if(!cmpUnitAI || !cmpUnitAI.BackToWork()) | if(!cmpUnitAI || !cmpUnitAI.BackToWork()) | ||||
▲ Show 20 Lines • Show All 196 Lines • ▼ Show 20 Lines | "garrison": function(player, cmd, data) | ||||
// Verify that the building can be controlled by the player or is mutualAlly | // Verify that the building can be controlled by the player or is mutualAlly | ||||
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) | if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) | ||||
{ | { | ||||
if (g_DebugCommands) | if (g_DebugCommands) | ||||
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); | warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); | ||||
return; | return; | ||||
} | } | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let pos = Engine.QueryInterface(cmd.target, IID_Position); | ||||
cmpUnitAI.Garrison(cmd.target, cmd.queued); | if (!pos) | ||||
return; | |||||
let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); | |||||
for (let i in unitAIs) | |||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.Garrison(cmd.target, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"guard": function(player, cmd, data) | "guard": function(player, cmd, data) | ||||
{ | { | ||||
// Verify that the target can be controlled by the player or is mutualAlly | // Verify that the target can be controlled by the player or is mutualAlly | ||||
if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) | if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) | ||||
{ | { | ||||
if (g_DebugCommands) | if (g_DebugCommands) | ||||
warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd)); | warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd)); | ||||
return; | return; | ||||
} | } | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | let pos = Engine.QueryInterface(cmd.target, IID_Position); | ||||
cmpUnitAI.Guard(cmd.target, cmd.queued); | if (!pos) | ||||
return; | |||||
let unitAIs = CreateGroupWalkOrderIfNecessary(data.entities, pos.GetPosition2D().x, pos.GetPosition2D().y, 14, cmd.queued); | |||||
for (let i in unitAIs) | |||||
unitAIs[i].forEach(cmpUnitAI => { | |||||
cmpUnitAI.Guard(cmd.target, cmd.queued || +i); | |||||
}); | }); | ||||
}, | }, | ||||
"stop": function(player, cmd, data) | "stop": function(player, cmd, data) | ||||
{ | { | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | data.entities.forEach(ent => { | ||||
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | |||||
if (cmpUnitAI) | |||||
cmpUnitAI.Stop(cmd.queued); | cmpUnitAI.Stop(cmd.queued); | ||||
}); | }); | ||||
}, | }, | ||||
"unload": function(player, cmd, data) | "unload": function(player, cmd, data) | ||||
{ | { | ||||
// Verify that the building can be controlled by the player or is mutualAlly | // Verify that the building can be controlled by the player or is mutualAlly | ||||
if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits)) | if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits)) | ||||
{ | { | ||||
▲ Show 20 Lines • Show All 75 Lines • ▼ Show 20 Lines | for (let ent of data.entities) | ||||
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); | var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); | ||||
if (cmpAlertRaiser) | if (cmpAlertRaiser) | ||||
cmpAlertRaiser.EndOfAlert(); | cmpAlertRaiser.EndOfAlert(); | ||||
} | } | ||||
}, | }, | ||||
"formation": function(player, cmd, data) | "formation": function(player, cmd, data) | ||||
{ | { | ||||
GetFormationUnitAIs(data.entities, player, cmd.name).forEach(cmpUnitAI => { | data.entities.forEach(ent => { | ||||
cmpUnitAI.MoveIntoFormation(cmd); | let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | ||||
if (cmpUnitAI) | |||||
cmpUnitAI.SetFormationTemplate(cmd.name); | |||||
}); | }); | ||||
}, | }, | ||||
"promote": function(player, cmd, data) | "promote": function(player, cmd, data) | ||||
{ | { | ||||
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); | var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); | ||||
cmpGuiInterface.PushNotification({ | cmpGuiInterface.PushNotification({ | ||||
"type": "aichat", | "type": "aichat", | ||||
Show All 32 Lines | for (let ent of data.entities) | ||||
cmpGate.LockGate(); | cmpGate.LockGate(); | ||||
else | else | ||||
cmpGate.UnlockGate(); | cmpGate.UnlockGate(); | ||||
} | } | ||||
}, | }, | ||||
"setup-trade-route": function(player, cmd, data) | "setup-trade-route": function(player, cmd, data) | ||||
{ | { | ||||
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { | data.entities.forEach(ent => { | ||||
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | |||||
if (cmpUnitAI) | |||||
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); | cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); | ||||
}); | }); | ||||
}, | }, | ||||
"set-trading-goods": function(player, cmd, data) | "set-trading-goods": function(player, cmd, data) | ||||
{ | { | ||||
data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); | data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); | ||||
}, | }, | ||||
▲ Show 20 Lines • Show All 211 Lines • ▼ Show 20 Lines | cmpGUIInterface.PushNotification({ | ||||
"type": "text", | "type": "text", | ||||
"players": [player], | "players": [player], | ||||
"message": "You can't raise the alert to a higher level!", | "message": "You can't raise the alert to a higher level!", | ||||
"translateMessage": true | "translateMessage": true | ||||
}); | }); | ||||
} | } | ||||
/** | /** | ||||
* Get some information about the formations used by entities. | |||||
* The entities must have a UnitAI component. | |||||
*/ | |||||
function ExtractFormations(ents) | |||||
{ | |||||
var entities = []; // subset of ents that have UnitAI | |||||
var members = {}; // { formationentity: [ent, ent, ...], ... } | |||||
for (let ent of ents) | |||||
{ | |||||
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | |||||
var fid = cmpUnitAI.GetFormationController(); | |||||
if (fid != INVALID_ENTITY) | |||||
{ | |||||
if (!members[fid]) | |||||
members[fid] = []; | |||||
members[fid].push(ent); | |||||
} | |||||
entities.push(ent); | |||||
} | |||||
var ids = [ id for (id in members) ]; | |||||
return { "entities": entities, "members": members, "ids": ids }; | |||||
} | |||||
/** | |||||
* Tries to find the best angle to put a dock at a given position | * Tries to find the best angle to put a dock at a given position | ||||
* Taken from GuiInterface.js | * Taken from GuiInterface.js | ||||
*/ | */ | ||||
function GetDockAngle(template, x, z) | function GetDockAngle(template, x, z) | ||||
{ | { | ||||
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); | var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); | ||||
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); | var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); | ||||
if (!cmpTerrain || !cmpWaterManager) | if (!cmpTerrain || !cmpWaterManager) | ||||
▲ Show 20 Lines • Show All 483 Lines • ▼ Show 20 Lines | else | ||||
{ | { | ||||
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); | error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
/** | function CreateGroupWalkOrderIfNecessary(ents, x, z, range, queued) | ||||
* Remove the given list of entities from their current formations. | |||||
*/ | |||||
function RemoveFromFormation(ents) | |||||
{ | { | ||||
var formation = ExtractFormations(ents); | // First, loop through units and check if all of them share a single non-null formation. | ||||
for (var fid in formation.members) | let formationTemplate = null; | ||||
for (let ent of ents) | |||||
{ | { | ||||
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); | let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | ||||
if (cmpFormation) | if (!cmpUnitAI) | ||||
cmpFormation.RemoveMembers(formation.members[fid]); | |||||
} | |||||
} | |||||
/** | |||||
* 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) | |||||
{ | { | ||||
// If an individual was selected, remove it from any formation | formationTemplate = "formations/null"; | ||||
// and command it individually | break; | ||||
if (ents.length == 1) | } | ||||
if (!formationTemplate) | |||||
formationTemplate = cmpUnitAI.GetFormationTemplate(); | |||||
if (formationTemplate == "formations/null") | |||||
break; | |||||
else if (cmpUnitAI.GetFormationTemplate() !== formationTemplate) | |||||
{ | { | ||||
// Skip unit if it has no UnitAI | formationTemplate = "formations/null"; | ||||
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); | break; | ||||
if (!cmpUnitAI) | } | ||||
return []; | } | ||||
RemoveFromFormation(ents); | let nonFormableUnitAIs = []; | ||||
let formableEntsID = []; | |||||
let formableEntsAI = []; | |||||
return [ cmpUnitAI ]; | // don't create a walk together order if this is a queued order because that's just going to be weird | ||||
} | let createGroupOrder = queued === false && formationTemplate !== "formations/null"; | ||||
// Separate out the units that don't support the chosen formation | |||||
var formedEnts = []; | |||||
var nonformedUnitAIs = []; | |||||
for (let ent of ents) | for (let ent of ents) | ||||
{ | { | ||||
// Skip units with no UnitAI or no position | let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | ||||
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | let cmpPosition = Engine.QueryInterface(ent, IID_Position); | ||||
var cmpPosition = Engine.QueryInterface(ent, IID_Position); | |||||
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) | if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) | ||||
continue; | continue; | ||||
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); | let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); | ||||
// TODO: We only check if the formation is usable by some units | if (createGroupOrder && cmpIdentity)// && cmpIdentity.CanUseFormation(formationTemplate)) | ||||
// if we move them to it. We should check if we can use formations | |||||
// for the other cases. | |||||
var nullFormation = (formationTemplate || cmpUnitAI.GetLastFormationTemplate()) == "formations/null"; | |||||
if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/null")) | |||||
formedEnts.push(ent); | |||||
else | |||||
{ | |||||
if (nullFormation) | |||||
cmpUnitAI.SetLastFormationTemplate("formations/null"); | |||||
nonformedUnitAIs.push(cmpUnitAI); | |||||
} | |||||
} | |||||
if (formedEnts.length == 0) | |||||
{ | |||||
// No units support the foundation - return all the others | |||||
return nonformedUnitAIs; | |||||
} | |||||
// Find what formations the formationable selected entities are currently in | |||||
var formation = ExtractFormations(formedEnts); | |||||
var formationUnitAIs = []; | |||||
if (formation.ids.length == 1) | |||||
{ | |||||
// Selected units either belong to this formation or have no formation | |||||
// Check that all its members are selected | |||||
var fid = formation.ids[0]; | |||||
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); | |||||
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length | |||||
&& cmpFormation.GetMemberCount() == formation.entities.length) | |||||
{ | { | ||||
cmpFormation.DeleteTwinFormations(); | formableEntsID.push(ent); | ||||
// The whole formation was selected, so reuse its controller for this command | formableEntsAI.push(cmpUnitAI); | ||||
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; | |||||
if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) | |||||
cmpFormation.LoadFormation(formationTemplate); | |||||
} | } | ||||
else | |||||
nonFormableUnitAIs.push(cmpUnitAI); | |||||
} | } | ||||
if (!formationUnitAIs.length) | // TODO: validate formation | ||||
{ | if (createGroupOrder && formableEntsAI.length > 1) | ||||
// We need to give the selected units a new formation controller | |||||
// Remove selected units from their current formation | |||||
for (var fid in formation.members) | |||||
{ | |||||
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); | |||||
if (cmpFormation) | |||||
cmpFormation.RemoveMembers(formation.members[fid]); | |||||
} | |||||
// 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 = []; | |||||
for (let cluster of clusters) | |||||
{ | |||||
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) | |||||
{ | |||||
// get the most recently used formation, or default to line closed | |||||
var lastFormationTemplate = undefined; | |||||
for (let ent of cluster) | |||||
{ | |||||
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); | |||||
if (cmpUnitAI) | |||||
{ | { | ||||
var template = cmpUnitAI.GetLastFormationTemplate(); | // TODO: get position, get obstruction, that kind of stuff. | ||||
if (lastFormationTemplate === undefined) | let cmpGroupWalkManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_GroupWalkManager); | ||||
{ | let groupID = cmpGroupWalkManager.CreateGroup(formableEntsID, x, z, range, formationTemplate); | ||||
lastFormationTemplate = template; | formableEntsAI.forEach(cmpUnitAI => { cmpUnitAI.GroupWalk(groupID); }); | ||||
} | |||||
else if (lastFormationTemplate != template) | |||||
{ | |||||
lastFormationTemplate = undefined; | |||||
break; | |||||
} | |||||
} | |||||
} | } | ||||
if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate)) | |||||
formationTemplate = lastFormationTemplate; | |||||
else | else | ||||
formationTemplate = "formations/null"; | |||||
} | |||||
// Create the new controller | |||||
var formationEnt = Engine.AddEntity(formationTemplate); | |||||
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); | |||||
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); | |||||
cmpFormation.SetFormationSeparation(formationSeparation); | |||||
cmpFormation.SetMembers(cluster); | |||||
for (let ent of formationEnts) | |||||
cmpFormation.RegisterTwinFormation(ent); | |||||
formationEnts.push(formationEnt); | |||||
var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); | |||||
cmpOwnership.SetOwner(player); | |||||
} | |||||
} | |||||
return nonformedUnitAIs.concat(formationUnitAIs); | |||||
} | |||||
/** | |||||
* Group a list of entities in clusters via single-links | |||||
*/ | |||||
function ClusterEntities(ents, separationDistance) | |||||
{ | |||||
var clusters = []; | |||||
if (!ents.length) | |||||
return clusters; | |||||
var distSq = separationDistance * separationDistance; | |||||
var positions = []; | |||||
// triangular matrix with the (squared) distances between the different clusters | |||||
// the other half is not initialised | |||||
var matrix = []; | |||||
for (let i = 0; i < ents.length; ++i) | |||||
{ | |||||
matrix[i] = []; | |||||
clusters.push([ents[i]]); | |||||
var cmpPosition = Engine.QueryInterface(ents[i], IID_Position); | |||||
positions.push(cmpPosition.GetPosition2D()); | |||||
for (let j = 0; j < i; ++j) | |||||
matrix[i][j] = positions[i].distanceToSquared(positions[j]); | |||||
} | |||||
while (clusters.length > 1) | |||||
{ | |||||
// search two clusters that are closer than the required distance | |||||
var closeClusters = undefined; | |||||
for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i) | |||||
for (var j = i - 1; j >= 0 && !closeClusters; --j) | |||||
if (matrix[i][j] < distSq) | |||||
closeClusters = [i,j]; | |||||
// if no more close clusters found, just return all found clusters so far | |||||
if (!closeClusters) | |||||
return clusters; | |||||
// make a new cluster with the entities from the two found clusters | |||||
var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); | |||||
// calculate the minimum distance between the new cluster and all other remaining | |||||
// clusters by taking the minimum of the two distances. | |||||
var distances = []; | |||||
for (let i = 0; i < clusters.length; ++i) | |||||
{ | { | ||||
if (i == closeClusters[1] || i == closeClusters[0]) | nonFormableUnitAIs = nonFormableUnitAIs.concat(formableEntsAI); | ||||
continue; | formableEntsAI = []; | ||||
var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]]; | |||||
var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]]; | |||||
distances.push(Math.min(dist1, dist2)); | |||||
} | |||||
// remove the rows and columns in the matrix for the merged clusters, | |||||
// and the clusters themselves from the cluster list | |||||
clusters.splice(closeClusters[0],1); | |||||
clusters.splice(closeClusters[1],1); | |||||
matrix.splice(closeClusters[0],1); | |||||
matrix.splice(closeClusters[1],1); | |||||
for (let i = 0; i < matrix.length; ++i) | |||||
{ | |||||
if (matrix[i].length > closeClusters[0]) | |||||
matrix[i].splice(closeClusters[0],1); | |||||
if (matrix[i].length > closeClusters[1]) | |||||
matrix[i].splice(closeClusters[1],1); | |||||
} | |||||
// add a new row of distances to the matrix and the new cluster | |||||
clusters.push(newCluster); | |||||
matrix.push(distances); | |||||
} | } | ||||
return clusters; | return [nonFormableUnitAIs, formableEntsAI]; | ||||
} | } | ||||
function GetFormationRequirements(formationTemplate) | function GetFormationRequirements(formationTemplate) | ||||
{ | { | ||||
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate); | var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate); | ||||
if (!template.Formation) | if (!template.Formation) | ||||
return false; | return false; | ||||
return { "minCount": +template.Formation.RequiredMemberCount }; | return { "minCount": +template.Formation.RequiredMemberCount }; | ||||
} | } | ||||
function CanMoveEntsIntoFormation(ents, formationTemplate) | function CanMoveEntsIntoFormation(ents, formationTemplate) | ||||
{ | { | ||||
// TODO: should check the player's civ is allowed to use this formation | // TODO: should check the player's civ is allowed to use this formation | ||||
// See simulation/components/Player.js GetFormations() for a list of all allowed formations | // See simulation/components/Player.js GetFormations() for a list of all allowed formations | ||||
var requirements = GetFormationRequirements(formationTemplate); | var requirements = GetFormationRequirements(formationTemplate); | ||||
if (!requirements) | if (!requirements) | ||||
return false; | return false; | ||||
▲ Show 20 Lines • Show All 56 Lines • Show Last 20 Lines |
Wildfire Games · Phabricator