Index: binaries/data/mods/public/gui/session/selection.js
===================================================================
--- binaries/data/mods/public/gui/session/selection.js
+++ binaries/data/mods/public/gui/session/selection.js
@@ -289,6 +289,7 @@
let i = 1;
let added = [];
+ ents = this.addBattalionMembers(ents);
for (let ent of ents)
{
@@ -332,6 +333,7 @@
EntitySelection.prototype.removeList = function(ents)
{
var removed = [];
+ ents = this.addBattalionMembers(ents);
for (let ent of ents)
if (this.selected[ent])
@@ -388,6 +390,7 @@
EntitySelection.prototype.setHighlightList = function(ents)
{
var highlighted = {};
+ ents = this.addBattalionMembers(ents);
for (let ent of ents)
highlighted[ent] = ent;
@@ -429,6 +432,21 @@
};
/**
+ * Adds the battalion members of a selected entities to the selection.
+ * @param {number[]} entities - The entity IDs of selected entities.
+ * @return {number[]} - Some more entity IDs if part of a battalion was selected.
+ */
+EntitySelection.prototype.addBattalionMembers = function(entities)
+{
+ let battalions = Engine.GuiInterfaceCall("GetBattalions", { "players": undefined });
+ for (let battalion of battalions)
+ if (entities.some(entity => battalion.entities.indexOf(entity) != -1))
+ entities = entities.concat(battalion.entities.filter(entity => entities.indexOf(entity) == -1));
+
+ return entities;
+};
+
+/**
* Cache some quantities which depends only on selection
*/
Index: binaries/data/mods/public/gui/session/selection_panels_helpers.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_panels_helpers.js
+++ binaries/data/mods/public/gui/session/selection_panels_helpers.js
@@ -278,6 +278,30 @@
});
}
+function formBattalion(entities)
+{
+ if (!entities)
+ return;
+
+ Engine.PostNetworkCommand({
+ "type": "battalion",
+ "entities": entities,
+ "name": "form"
+ });
+}
+
+function disbandBattalion(battalions)
+{
+ if (!battalions)
+ return;
+
+ Engine.PostNetworkCommand({
+ "type": "battalion",
+ "battalions": battalions,
+ "name": "disband"
+ });
+}
+
function performStance(entities, stanceName)
{
if (!entities)
Index: binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- binaries/data/mods/public/gui/session/unit_actions.js
+++ binaries/data/mods/public/gui/session/unit_actions.js
@@ -1095,6 +1095,58 @@
},
},
+ "formBattalion": {
+ "getInfo": function(entStates)
+ {
+ // Return false when multiple types of units are selected.
+ //let refTemplateName = entStates[0].template.selectionGroupName || entStates[0].template;
+ if (entStates.length < 2 //||
+ //entStates.some(entState =>
+ // (entState.template.selectionGroupName || entState.template) != refTemplateName
+ //)
+ )
+ return false;
+
+ return {
+ "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.formbattalion") +
+ translate("Form a battalion with the currently selected units."),
+ "icon": "training.png"
+ };
+ },
+ "execute": function(entStates)
+ {
+ if (entStates.length)
+ formBattalion(entStates.map(entState => entState.id));
+ },
+ },
+
+ "disbandBattalion": {
+ "getInfo": function(entStates)
+ {
+ // Only return false when no battalions are selected.
+ if (entStates.every(entState =>
+ entState.battalion == false
+ ))
+ return false;
+
+ return {
+ "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.disbandbattalion") +
+ translate("Disband all currently selected battalion(s)."),
+ "icon": "cancel.png"
+ };
+ },
+ "execute": function(entStates)
+ {
+ // Pass only the battalion IDs to the disband function.
+ if (entStates.length)
+ disbandBattalion(entStates.map(entState => {
+ let bat = entState.battalion;
+ if (bat)
+ return bat;
+ }));
+ },
+ },
+
"garrison": {
"getInfo": function(entStates)
{
Index: binaries/data/mods/public/simulation/components/B.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/components/B.js
@@ -0,0 +1,213 @@
+function BattalionManager() {}
+
+BattalionManager.prototype.Schema =
+ "";
+
+BattalionManager.prototype.Init = function()
+{
+ this.battalions = [];
+};
+
+/**
+ * Creates a new battalion.
+ * This now reuses IDs of disbanded battalions.
+ * @param {number[]} entities - The entity IDs to form the battalion with.
+ */
+BattalionManager.prototype.FormBattalion = function(player, entities)
+{
+ // Use the first free battalion spot.
+ // If the IDs are required for more components in the future one might consider
+ // giving them truly unique IDs (keeping them incrementing like entity IDs).
+ // We fill the gaps now, by checking if the ID is a valid group, if there
+ // are no gaps, the highest value ID would need reach should be #groups + 1.
+ let index = this.battalions.length;
+ for (let id = 1; index + 1; id++)
+ {
+ if (this.battalions.some(battalion => battalion.id == id))
+ continue;
+
+ this.battalions[index] = {
+ "id": id,
+ "player": player,
+ "entities": []
+ };
+ this.AddEntities(id, entities);
+ break;
+ }
+};
+
+/**
+ * Removes battalion(s).
+ * @param {number[]} battalionIDs - The IDs of the battalions. If undefinded all battalions will be removed.
+ */
+BattalionManager.prototype.DisbandBattalions = function(battalionIDs = undefined)
+{
+ if (battalionIDs)
+ this.battalions = this.battalions.filter(battalion => battalionIDs.indexOf(battalion.id) == -1);
+ else
+ this.battalions = [];
+};
+
+/**
+ * Adds an entity/entities to the battalion.
+ * @param {number} battalionID - The unique ID of the battalion.
+ * @param {number} battalionID - The index of the battalion, passed by "formBattalion".
+ * @param {number[]} entities - The creatures from LOTR.
+ */
+BattalionManager.prototype.AddEntities = function(battalionID, entities)
+{
+ let battalion = this.battalions.find(batt => batt.id == battalionID);
+ if (!battalion)
+ {
+ error("Entity/Entities " + entities + " could not be added to battalion " + battalionID + " for that battalion does not exist.");
+ return;
+ }
+ let index = this.battalions.indexOf(battalion);
+ for (let entity of entities)
+ {
+ // Entities cannot be in more than one battalion at the same time.
+ let bat = this.IsPartOfBattalion(entity);
+ if (bat)
+ this.RemoveEntities(bat, [entity]);
+
+ if (this.battalions[index].entities.indexOf(entity) != -1)
+ continue;
+
+ let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ let owner = this.battalions[index].player;
+ let entState = cmpGuiInterface.GetEntityState(owner, entity);
+
+ // When this function is called during group rebuild, deleted
+ // entities will not yet have been removed, so entities might
+ // still be present in the group despite not existing.
+ if (!entState)
+ continue;
+
+ let templateName = entState.template;
+ let key = entState.selectionGroupName || templateName;
+ this.battalions[index].entities.push(entity);
+ if (!this.battalions[index].template)
+ this.battalions[index].template = key;
+ }
+};
+
+/**
+ * Removes an entity/entities from the battalion.
+ * @param {number} battalionID - The unique ID of the battalion.
+ * @param {number} entities - The entity ID of the entities to remove.
+ */
+BattalionManager.prototype.RemoveEntities = function(battalionID, entities)
+{
+ for (let battalion of this.battalions)
+ {
+ if (battalion.id != battalionID)
+ continue;
+ let battalionIndex = this.battalions.indexOf(battalion);
+ for (let entity of entities)
+ {
+ let entIndex = battalion.entities.indexOf(entity);
+ if (entIndex != -1)
+ this.battalions[battalionIndex].entities.splice(entIndex, 1);
+ else
+ warn("Entity " + entity + " could not removed from battalion " + battalion.id + " for it is no part of it.");
+ }
+ }
+};
+
+/**
+ * Check wheter an entity is part of an battalion.
+ * @param {number} entity - The entity ID to check.
+ * @return {number|boolean} - Either a number when the entity is part of a battalion
+ * or false when it is not.
+ */
+BattalionManager.prototype.IsPartOfBattalion = function(entity)
+{
+ for (let battalion of this.battalions)
+ if (battalion.entities.indexOf(entity) != -1)
+ return battalion.id;
+
+ return false;
+};
+
+/**
+ * Update the battalions. E.g. when units die or change ownership and such.
+ * ToDo: Run this when necessary.
+ */
+BattalionManager.prototype.Update = function()
+{
+ this.CheckRenamedEntities();
+ let battalionsToDisband = [];
+ let battalions = this.battalions;
+ for (let battalion of battalions)
+ {
+ let entitiesToRemove = [];
+ for (let entity of battalion.entities)
+ {
+ let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ let entState = cmpGuiInterface.GetEntityState(battalion.player, entity);
+ // Remove deleted units or now unowned units.
+ if (!entState || entState.player != battalion.player)
+ entitiesToRemove.push(entity);
+ }
+ this.RemoveEntities(battalion.id, entitiesToRemove);
+
+ if (!battalion.entities.length)
+ battalionsToDisband.push(battalion.id);
+ }
+ if (battalionsToDisband.length)
+ this.DisbandBattalions(battalionsToDisband);
+};
+
+/**
+ * Update battalions if some entities in the battalion were renamed
+ * (in case of unit promotion or finishing building structure).
+ * ToDo: This needs some love.
+ */
+BattalionManager.prototype.CheckRenamedEntities = function()
+{
+ let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ // Reconstruct groups if at least one entity has been renamed.
+ let renamedEntities = cmpGuiInterface.GetRenamedEntities();
+ if (renamedEntities.length > 0)
+ {
+ for (let battalion of this.battalions)
+ {
+ renamedEntities = cmpGuiInterface.GetRenamedEntities(battalion.player);
+ for (let renamedEntity of renamedEntities)
+ {
+ if (battalion.entities.indexOf(renamedEntity.entity) != -1)
+ {
+ this.RemoveEntities(battalion.id, [renamedEntity.entity]);
+ let cmpOwner = Engine.QueryInterface(renamedEntity.newentity, IID_Ownership);
+ if (cmpOwner && cmpOwner.GetOwner() == battalion.player)
+ this.AddEntities(battalion.id, [renamedEntity.newentity]);
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Return the battalion(s).
+ * @param {number[]} players - Optionally the players we want to check.
+ * @return {object[]} battalions - The currently used battalions.
+ */
+BattalionManager.prototype.GetBattalions = function(players = undefined)
+{
+ if (!players)
+ return this.battalions;
+
+ return this.battalions.filter(battalion => players.indexOf(battalion.player) != -1);
+};
+
+
+/**
+ * Subscribe to all changes in ownership and such.
+ */
+BattalionManager.prototype.OnGlobalOwnershipChanged = function(msg)
+{
+ if (this.IsPartOfBattalion(msg.entity))
+ this.Update();
+};
+
+Engine.RegisterSystemComponentType(IID_BattalionManager, "BattalionManager", BattalionManager);
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -510,6 +510,9 @@
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
+ let cmpBattalionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_BattalionManager);
+ ret.battalion = cmpBattalionManager.IsPartOfBattalion(ent);
+
return ret;
};
@@ -903,6 +906,11 @@
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
+GuiInterface.prototype.GetBattalions = function(player, data)
+{
+ return Engine.QueryInterface(SYSTEM_ENTITY, IID_BattalionManager).GetBattalions(data.players);
+};
+
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
@@ -1934,6 +1942,7 @@
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
+ "GetBattalions": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
Index: binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- binaries/data/mods/public/simulation/components/ProductionQueue.js
+++ binaries/data/mods/public/simulation/components/ProductionQueue.js
@@ -656,6 +656,13 @@
"metadata": metadata,
});
+ if (false && createdEnts.length > 1)
+ {
+ let cmpBattalions = Engine.QueryInterface(SYSTEM_ENTITY, IID_Battalions)
+ cmpBattalions.FormBattalion(cmpOwnership.GetOwner(), createdEnts);
+ cmpBattalions.Update();
+ }
+
return createdEnts.length;
};
Index: binaries/data/mods/public/simulation/components/interfaces/B.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/components/interfaces/B.js
@@ -0,0 +1 @@
+Engine.RegisterInterface("BattalionManager");
Index: binaries/data/mods/public/simulation/components/tests/test_B.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/components/tests/test_B.js
@@ -0,0 +1,74 @@
+Engine.LoadComponentScript("interfaces/B.js");
+Engine.LoadComponentScript("B.js");
+
+let cmpBattalionManager = ConstructComponent(SYSTEM_ENTITY, "BattalionManager");
+
+let player = 1;
+let group1 = [2, 3, 4, 5];
+let group2 = [6, 7, 8, 9];
+let group3 = [5, 6, 10, 11];
+let allEnts = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+let template = "{civ}/{faction}/{entity}/{rank}";
+let GUItemplate = {
+ "template": template,
+ "player": player
+};
+
+AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
+ "GetEntityState": (owner, entity) => GUItemplate,
+ "GetRenamedEntities": () => []
+});
+
+// No entity is in a battalion yet.
+for (let i of allEnts)
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), false);
+TS_ASSERT_UNEVAL_EQUALS(cmpBattalionManager.GetBattalions(), []);
+
+cmpBattalionManager.FormBattalion(player, group1);
+for (let i of group1)
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), 1);
+
+cmpBattalionManager.FormBattalion(player, group2);
+for (let i of group2)
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), 2);
+
+// No battalions for player 2.
+TS_ASSERT_UNEVAL_EQUALS(cmpBattalionManager.GetBattalions([2]), []);
+
+// Here we form a battalion with parts of another battalion, in-game this should only
+// happen when merging complete battalions, since they are only fully selectable.
+cmpBattalionManager.FormBattalion(player, group3);
+for (let i of group3)
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), 3);
+for (let i of group1)
+ if (i == 5)
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), 3);
+ else
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), 1);
+for (let i of group2)
+ if (i == 6)
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), 3);
+ else
+ TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(i), 2);
+
+cmpBattalionManager.DisbandBattalions([1]);
+let result = [
+ { "id":2, "player": player, "entities": [7, 8, 9], "template": template },
+ { "id":3, "player": player, "entities": [5, 6, 10, 11], "template": template }
+];
+TS_ASSERT_UNEVAL_EQUALS(cmpBattalionManager.GetBattalions(), result);
+
+// Verify that a removed entity is indeed not part of a battalion anymore.
+cmpBattalionManager.RemoveEntities(2, [8]);
+TS_ASSERT_EQUALS(cmpBattalionManager.IsPartOfBattalion(8), false);
+
+// Verify that once the last entity of a battalion is removed the battalion is disbanded.
+cmpBattalionManager.RemoveEntities(3, [5, 6, 10, 11]);
+cmpBattalionManager.Update();
+result = [{ "id":2, "player": player, "entities": [7, 9], "template": template }];
+TS_ASSERT_UNEVAL_EQUALS(cmpBattalionManager.GetBattalions(), result);
+
+cmpBattalionManager.FormBattalion(player, allEnts);
+cmpBattalionManager.Update();
+result = [{ "id":1, "player": player, "entities": allEnts, "template": template }];
+TS_ASSERT_UNEVAL_EQUALS(cmpBattalionManager.GetBattalions(), result);
Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
+++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
@@ -34,6 +34,7 @@
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
+Engine.LoadComponentScript("interfaces/B.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
@@ -560,6 +561,10 @@
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
+AddMock(SYSTEM_ENTITY, IID_BattalionManager, {
+ "IsPartOfBattalion": () => false
+});
+
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
@@ -586,5 +591,6 @@
"resourceTrickle": {
"interval": 1250,
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
- }
+ },
+ "battalion": false
});
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js
+++ binaries/data/mods/public/simulation/helpers/Commands.js
@@ -627,6 +627,16 @@
}
},
+ "battalion": function(player, cmd, data)
+ {
+ let cmpBattalionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_BattalionManager);
+ if (cmd.name == "form")
+ cmpBattalionManager.FormBattalion(player, data.entities);
+ if (cmd.name == "disband")
+ cmpBattalionManager.DisbandBattalions(cmd.battalions)
+ cmpBattalionManager.Update();
+ },
+
"lock-gate": function(player, cmd, data)
{
for (let ent of data.entities)