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)