Index: ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js +++ ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js @@ -9,13 +9,16 @@ * Returns modified property value modified by the applicable tech * modifications. * - * @param currentTechModifications array of modificiations + * @param modifications array of modificiations * @param classes Array containing the class list of the template. * @param originalValue Number storing the original value. Can also be - * non-numberic, but then only "replace" techs can be supported. + * non-numeric, but then only "replace" and "tokens" techs can be supported. */ function GetTechModifiedProperty(modifications, classes, originalValue) { + if (!modifications.length) + return originalValue; + let multiply = 1; let add = 0; @@ -25,6 +28,8 @@ continue; if (modification.replace !== undefined) return modification.replace; + if (modification.tokens !== undefined) + return HandleTokens(originalValue, modification.tokens); if (modification.multiply) multiply *= modification.multiply; else if (modification.add) @@ -32,11 +37,7 @@ else warn("GetTechModifiedProperty: modification format not recognised : " + uneval(modification)); } - - // Note, some components pass non-numeric values (for which only the "replace" modification makes sense) - if (typeof originalValue == "number") - return originalValue * multiply + add; - return originalValue; + return originalValue * multiply + add; } /** @@ -48,6 +49,35 @@ } /** + * Returns a modified list of tokens. + * Supports "A>B" to replace A by B, "-A" to remove A, and the rest will add tokens. + */ +function HandleTokens(originalValue, modification) +{ + let tokens = originalValue === "" ? [] : originalValue.split(/\s+/); + let newTokens = modification === "" ? [] : modification.split(/\s+/); + for (let token of newTokens) + { + if (token.indexOf(">") !== -1) + { + let [oldToken, newToken] = token.split(">"); + let index = tokens.indexOf(oldToken); + if (index !== -1) + tokens[index] = newToken; + } + else if (token[0] == "-") + { + let index = tokens.indexOf(token.substr(1)); + if (index !== -1) + tokens.splice(index, 1); + } + else + tokens.push(token); + } + return tokens.join(" "); +} + +/** * Derives the technology requirements from a given technology template. * Takes into account the `supersedes` attribute. * Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js @@ -214,12 +214,6 @@ if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, - "resetselectionpannel": function(notification, player) - { - if (player != Engine.GetPlayerID()) - return; - g_Selection.rebuildSelection({}); - }, "playercommand": function(notification, player) { // For observers, focus the camera on units commanded by the selected player Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js +++ ps/trunk/binaries/data/mods/public/gui/session/session.js @@ -620,6 +620,13 @@ } g_SimState = undefined; + // Some changes may require re-rendering the selection. + if (Engine.GuiInterfaceCall("IsSelectionDirty")) + { + g_Selection.onChange(); + Engine.GuiInterfaceCall("ResetSelectionDirty"); + } + if (!GetSimState()) return; Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -15,8 +15,7 @@ this._tpCache = new Map(); }, - // helper function to return a template value, optionally adjusting for tech. - // TODO: there's no support for "_string" values here. + // Helper function to return a template value, adjusting for tech. "get": function(string) { let value = this._template; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/buildManager.js @@ -79,6 +79,21 @@ if (ent && ent.hasClass("Unit")) this.incrementBuilderCounters(civ, ent, increment); } + + for (let evt of events.ValueModification) + { + if (evt.component != "Builder" || + !evt.valueNames.some(val => val.startsWith("Builder/Entities/"))) + continue; + + // Unfortunately there really is not an easy way to determine the changes + // at this stage, so we simply have to dump the cache. + this.builderCounters = new Map(); + + let civ = gameState.getPlayerCiv(); + for (let ent of gameState.getOwnUnits().values()) + this.incrementBuilderCounters(civ, ent, 1); + } }; Index: ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js @@ -14,6 +14,7 @@ "AIMetadata", "PlayerDefeated", "EntityRenamed", + "ValueModification", "OwnershipChanged", "Garrison", "UnGarrison", @@ -251,7 +252,7 @@ if (!ended) continue; // item now contains the template value for this. - let oldValue = +item; + let oldValue = +item == item ? +item : item; let newValue = ApplyValueModificationsToTemplate(valName, oldValue, msg.player, template); // Apply the same roundings as in the components if (valName === "Player/MaxPopulation" || valName === "Cost/Population" || @@ -273,6 +274,7 @@ AIInterface.prototype.OnGlobalValueModification = function(msg) { + this.events.ValueModification.push(msg); let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); for (let ent of msg.entities) { @@ -299,7 +301,7 @@ if (!ended) continue; // "item" now contains the unmodified template value for this. - let oldValue = +item; + let oldValue = +item == item ? +item : item; let newValue = ApplyValueModificationsToEntity(valName, oldValue, ent); // Apply the same roundings as in the components if (valName === "Player/MaxPopulation" || valName === "Cost/Population" || Index: ps/trunk/binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Builder.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Builder.js @@ -34,6 +34,8 @@ if (!cmpPlayer) return []; + string = ApplyValueModificationsToEntity("Builder/Entities/_string", string, this.entity); + let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{native\}/g, cmpIdentity.GetCiv()); @@ -85,4 +87,15 @@ } }; +Builder.prototype.OnValueModification = function(msg) +{ + if (msg.component != "Builder" || !msg.valueNames.some(name => name.endsWith('_string'))) + return; + + // Token changes may require selection updates. + let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + if (cmpPlayer) + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); +}; + Engine.RegisterComponentType(IID_Builder, "Builder", Builder); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -35,6 +35,7 @@ this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; this.templateModified = {}; + this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; @@ -659,6 +660,7 @@ GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; + this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) @@ -672,6 +674,35 @@ }; /** + * Some changes may require an update to the selection panel, + * which is cached for efficiency. Inform the GUI it needs reloading. + */ +GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg) +{ + this.selectionDirty[msg.player] = true; +}; + +GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg) +{ + this.selectionDirty[msg.player] = true; +}; + +GuiInterface.prototype.SetSelectionDirty = function(player) +{ + this.selectionDirty[player] = true; +}; + +GuiInterface.prototype.IsSelectionDirty = function(player) +{ + return this.selectionDirty[player] || false; +}; + +GuiInterface.prototype.ResetSelectionDirty = function() +{ + this.selectionDirty = {}; +}; + +/** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) @@ -1991,7 +2022,9 @@ "GetTraderNumber": 1, "GetTradingGoods": 1, "IsTemplateModified": 1, - "ResetTemplateModified": 1 + "ResetTemplateModified": 1, + "IsSelectionDirty": 1, + "ResetSelectionDirty": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js @@ -863,23 +863,12 @@ { this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); - var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGuiInterface.PushNotification({ - "type": "resetselectionpannel", - "players": [this.GetPlayerID()] - }); }; Player.prototype.RemoveDisabledTemplate = function(template) { this.disabledTemplates[template] = false; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); - - var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGuiInterface.PushNotification({ - "type": "resetselectionpannel", - "players": [this.GetPlayerID()] - }); }; Player.prototype.SetDisabledTemplates = function(templates) @@ -888,12 +877,6 @@ for (let template of templates) this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {}); - - var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGuiInterface.PushNotification({ - "type": "resetselectionpannel", - "players": [this.GetPlayerID()] - }); }; Player.prototype.GetDisabledTemplates = function() Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -75,60 +75,122 @@ */ ProductionQueue.prototype.GetEntitiesList = function() { - return this.entitiesList; + return Array.from(this.entitiesMap.values()); }; -ProductionQueue.prototype.CalculateEntitiesList = function() +/** + * Calculate the new list of producible entities + * and update any entities currently being produced. + */ +ProductionQueue.prototype.CalculateEntitiesMap = function() { - this.entitiesList = []; + // Don't reset the map, it's used below to update entities. + if (!this.entitiesMap) + this.entitiesMap = new Map(); if (!this.template.Entities) return; let string = this.template.Entities._string; - if (!string) + // Tokens can be added -> process an empty list to get them. + let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity); + if (!addedTokens && !string) return; - // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. + addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/); + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) - return; - let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - if (cmpIdentity) - string = string.replace(/\{native\}/g, cmpIdentity.GetCiv()); - - let entitiesList = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/); - // Filter out disabled and invalid entities. - let disabledEntities = cmpPlayer.GetDisabledTemplates(); - entitiesList = entitiesList.filter(ent => !disabledEntities[ent] && cmpTemplateManager.TemplateExists(ent)); + let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {}; - // Check if some templates need to show their advanced or elite version. - let upgradeTemplate = function(templateName) - { - let template = cmpTemplateManager.GetTemplate(templateName); - while (template && template.Promotion !== undefined) - { - let requiredXp = ApplyValueModificationsToTemplate( - "Promotion/RequiredXp", - +template.Promotion.RequiredXp, - cmpPlayer.GetPlayerID(), - template); - if (requiredXp > 0) - break; - templateName = template.Promotion.Entity; - template = cmpTemplateManager.GetTemplate(templateName); - } - return templateName; + /** + * Process tokens: + * - process token modifiers (this is a bit tricky). + * - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID + * - remove disabled entities + * - upgrade templates where necessary + * This also updates currently queued production (it's more convenient to do it here). + */ + + let removeAllQueuedTemplate = (token) => { + let queue = clone(this.queue); + let template = this.entitiesMap.get(token); + for (let item of queue) + if (item.unitTemplate && item.unitTemplate === template) + this.RemoveBatch(item.id); + }; + let updateAllQueuedTemplate = (token, updateTo) => { + let template = this.entitiesMap.get(token); + for (let item of this.queue) + if (item.unitTemplate && item.unitTemplate === template) + item.unitTemplate = updateTo; }; - for (let templateName of entitiesList) - this.entitiesList.push(upgradeTemplate(templateName)); + let toks = string.split(/\s+/); + for (let tok of addedTokens) + toks.push(tok); + + let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {}); + this.entitiesMap = toks.reduce((entMap, token) => { + let rawToken = token; + if (!(token in addedDict)) + { + // This is a bit wasteful but I can't think of a simpler/better way. + // The list of token is unlikely to be a performance bottleneck anyways. + token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity); + token = token.split(/\s+/); + if (token.every(tok => addedTokens.indexOf(tok) !== -1)) + { + removeAllQueuedTemplate(rawToken); + return entMap; + } + token = token[0]; + } + // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. + if (cmpIdentity) + token = token.replace(/\{native\}/g, cmpIdentity.GetCiv()); + if (cmpPlayer) + token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv()); + + // Filter out disabled and invalid entities. + if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token)) + { + removeAllQueuedTemplate(rawToken); + return entMap; + } + + token = this.GetUpgradedTemplate(token); + entMap.set(rawToken, token); + updateAllQueuedTemplate(rawToken, token); + return entMap; + }, new Map()); +}; - for (let item of this.queue) - if (item.unitTemplate) - item.unitTemplate = upgradeTemplate(item.unitTemplate); +/* + * Returns the upgraded template name if necessary. + */ +ProductionQueue.prototype.GetUpgradedTemplate = function(templateName) +{ + let cmpPlayer = QueryOwnerInterface(this.entity); + if (!cmpPlayer) + return templateName; + + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + let template = cmpTemplateManager.GetTemplate(templateName); + while (template && template.Promotion !== undefined) + { + let requiredXp = ApplyValueModificationsToTemplate( + "Promotion/RequiredXp", + +template.Promotion.RequiredXp, + cmpPlayer.GetPlayerID(), + template); + if (requiredXp > 0) + break; + templateName = template.Promotion.Entity; + template = cmpTemplateManager.GetTemplate(templateName); + } + return templateName; }; /* @@ -140,6 +202,8 @@ return []; let string = this.template.Technologies._string; + string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity); + if (!string) return []; @@ -148,8 +212,7 @@ return []; let cmpPlayer = QueryOwnerInterface(this.entity); - let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - if (!cmpPlayer || !cmpIdentity) + if (!cmpPlayer) return []; let techs = string.split(/\s+/); @@ -558,7 +621,7 @@ cmpPlayer.UnBlockTraining(); } if (msg.to != INVALID_PLAYER) - this.CalculateEntitiesList(); + this.CalculateEntitiesMap(); // Reset the production queue whenever the owner changes. // (This should prevent players getting surprised when they capture @@ -570,7 +633,7 @@ ProductionQueue.prototype.OnCivChanged = function() { - this.CalculateEntitiesList(); + this.CalculateEntitiesMap(); }; ProductionQueue.prototype.OnDestroy = function() @@ -866,15 +929,29 @@ // If the promotion requirements of units is changed, // update the entities list so that automatically promoted units are shown // appropriately in the list. - if (msg.component == "Promotion") - this.CalculateEntitiesList(); + if (msg.component != "Promotion" && (msg.component != "ProductionQueue" || + !msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/")))) + return; + + if (msg.entities.indexOf(this.entity) === -1) + return; + + // This also updates the queued production if necessary. + this.CalculateEntitiesMap(); + + // Inform the GUI that it'll need to recompute the selection panel. + // TODO: it would be better to only send the message if something actually changing + // for the current production queue. + let cmpPlayer = QueryOwnerInterface(this.entity); + if (cmpPlayer) + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); }; ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg) { // If the disabled templates of the player is changed, // update the entities list so that this is reflected there. - this.CalculateEntitiesList(); + this.CalculateEntitiesMap(); }; Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Builder.js @@ -1,4 +1,3 @@ -Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("Builder.js"); @@ -11,6 +10,9 @@ "TemplateExists": () => true }); +Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal); + + let cmpBuilder = ConstructComponent(builderId, "Builder", { "Rate": 1.0, "Entities": { "_string": "structures/{civ}_barracks structures/{civ}_civil_centre structures/{native}_house" } Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js @@ -1,158 +1,158 @@ -Resources = { - "BuildSchema": (a, b) => {} -}; - Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); -Engine.LoadComponentScript("ProductionQueue.js"); +Engine.LoadComponentScript("interfaces/BuildRestrictions.js"); +Engine.LoadComponentScript("interfaces/EntityLimits.js"); +Engine.LoadComponentScript("interfaces/Foundation.js"); +Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); +Engine.LoadComponentScript("interfaces/Trigger.js"); +Engine.LoadComponentScript("EntityLimits.js"); -global.TechnologyTemplates = { - "Has": name => name == "phase_town_athen" || name == "phase_city_athen", - "Get": () => ({}) -}; - -const productionQueueId = 6; -const playerId = 1; -const playerEntityID = 2; - -AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({}) +Engine.RegisterGlobal("Resources", { + "BuildSchema": (a, b) => {} }); +Engine.LoadComponentScript("ProductionQueue.js"); +Engine.LoadComponentScript("TrainingRestrictions.js"); -let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", { - "Entities": { "_string": "units/{civ}_cavalry_javelinist_b " + - "units/{civ}_infantry_swordsman_b " + - "units/{native}_support_female_citizen" }, - "Technologies": { "_string": "gather_fishing_net " + - "phase_town_{civ} " + - "phase_city_{civ}" } -}); +Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); +Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value); -cmpProductionQueue.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), []); +function testEntitiesList() +{ + Engine.RegisterGlobal("TechnologyTemplates", { + "Has": name => name == "phase_town_athen" || name == "phase_city_athen", + "Get": () => ({}) + }); -AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEntityID -}); + const productionQueueId = 6; + const playerId = 1; + const playerEntityID = 2; -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({}), - "GetPlayerID": () => playerId -}); + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "GetTemplate": name => ({}) + }); -AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, - "IsInProgress": () => false, - "IsTechnologyResearched": () => false -}); + let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", { + "Entities": { "_string": "units/{civ}_cavalry_javelinist_b " + + "units/{civ}_infantry_swordsman_b " + + "units/{native}_support_female_citizen" }, + "Technologies": { "_string": "gather_fishing_net " + + "phase_town_{civ} " + + "phase_city_{civ}" } + }); + cmpProductionQueue.GetUpgradedTemplate = (template) => template; -AddMock(productionQueueId, IID_Ownership, { - "GetOwner": () => playerId -}); + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => playerEntityID + }); -AddMock(productionQueueId, IID_Identity, { - "GetCiv": () => "iber" -}); + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}), + "GetDisabledTemplates": () => ({}), + "GetPlayerID": () => playerId + }); -cmpProductionQueue.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"] -); -TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] -); - -AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": name => name == "units/iber_support_female_citizen", - "GetTemplate": name => ({}) -}); + AddMock(playerEntityID, IID_TechnologyManager, { + "CheckTechnologyRequirements": () => true, + "IsInProgress": () => false, + "IsTechnologyResearched": () => false + }); -cmpProductionQueue.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber_support_female_citizen"]); + AddMock(productionQueueId, IID_Ownership, { + "GetOwner": () => playerId + }); -AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({}) -}); + AddMock(productionQueueId, IID_Identity, { + "GetCiv": () => "iber" + }); -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }), - "GetPlayerID": () => playerId -}); + cmpProductionQueue.CalculateEntitiesMap(); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetEntitiesList(), + ["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"] + ); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetTechnologiesList(), + ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] + ); -cmpProductionQueue.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"] -); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/iber_infantry_swordsman_b": true }), - "GetPlayerID": () => playerId -}); + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": name => name == "units/iber_support_female_citizen", + "GetTemplate": name => ({}) + }); -cmpProductionQueue.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber_cavalry_javelinist_b", "units/iber_support_female_citizen"] -); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "athen", - "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }), - "GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }), - "GetPlayerID": () => playerId -}); + cmpProductionQueue.CalculateEntitiesMap(); + TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber_support_female_citizen"]); -cmpProductionQueue.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/athen_cavalry_javelinist_b", "units/iber_support_female_citizen"] -); -TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), ["phase_town_athen", - "phase_city_athen"] -); - -AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, - "IsInProgress": () => false, - "IsTechnologyResearched": tech => tech == "phase_town_athen" -}); -TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), [undefined, "phase_city_athen"]); + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "GetTemplate": name => ({}) + }); -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetPlayerID": () => playerId -}); -TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] -); + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}), + "GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }), + "GetPlayerID": () => playerId + }); -Engine.LoadComponentScript("interfaces/BuildRestrictions.js"); -Engine.LoadComponentScript("interfaces/EntityLimits.js"); -Engine.LoadComponentScript("interfaces/Foundation.js"); -Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); -Engine.LoadComponentScript("interfaces/Timer.js"); -Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); -Engine.LoadComponentScript("interfaces/Trigger.js"); -Engine.LoadComponentScript("EntityLimits.js"); -Engine.LoadComponentScript("TrainingRestrictions.js"); -Engine.LoadHelperScript("Sound.js"); + cmpProductionQueue.CalculateEntitiesMap(); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetEntitiesList(), + ["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"] + ); -Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); -Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value); + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}), + "GetDisabledTemplates": () => ({ "units/iber_infantry_swordsman_b": true }), + "GetPlayerID": () => playerId + }); + + cmpProductionQueue.CalculateEntitiesMap(); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetEntitiesList(), + ["units/iber_cavalry_javelinist_b", "units/iber_support_female_citizen"] + ); + + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "athen", + "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }), + "GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }), + "GetPlayerID": () => playerId + }); + + cmpProductionQueue.CalculateEntitiesMap(); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetEntitiesList(), + ["units/athen_cavalry_javelinist_b", "units/iber_support_female_citizen"] + ); + TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), ["phase_town_athen", + "phase_city_athen"] + ); + + AddMock(playerEntityID, IID_TechnologyManager, { + "CheckTechnologyRequirements": () => true, + "IsInProgress": () => false, + "IsTechnologyResearched": tech => tech == "phase_town_athen" + }); + TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), [undefined, "phase_city_athen"]); + + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}), + "GetPlayerID": () => playerId + }); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetTechnologiesList(), + ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] + ); +} function regression_test_d1879() { @@ -280,4 +280,74 @@ TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6); } +function test_token_changes() +{ + const ent = 10; + let cmpProductionQueue = ConstructComponent(10, "ProductionQueue", { + "Entities": { "_string": "units/{civ}_a " + + "units/{civ}_b" }, + "Technologies": { "_string": "a " + + "b_{civ} " + + "c_{civ}" }, + "BatchTimeModifier": 1 + }); + cmpProductionQueue.GetUpgradedTemplate = (template) => template; + + // Merges interface of multiple components because it's enough here. + Engine.RegisterGlobal("QueryOwnerInterface", () => ({ + // player + "GetCiv": () => "test", + "GetDisabledTemplates": () => [], + "GetDisabledTechnologies": () => [], + "TrySubtractResources": () => true, + "AddResources": () => {}, + "GetPlayerID": () => 1, + // entitylimits + "ChangeCount": () => {}, + // techmanager + "CheckTechnologyRequirements": () => true, + "IsTechnologyResearched": () => false, + "IsInProgress": () => false + })); + Engine.RegisterGlobal("QueryPlayerIDInterface", QueryOwnerInterface); + + AddMock(SYSTEM_ENTITY, IID_GuiInterface, { + "SetSelectionDirty": () => {} + }); + + // Test Setup + cmpProductionQueue.CalculateEntitiesMap(); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetEntitiesList(), ["units/test_a", "units/test_b"] + ); + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetTechnologiesList(), ["a", "b_generic", "c_generic"] + ); + // Add a unit of each type to our queue, validate. + cmpProductionQueue.AddBatch("units/test_a", "unit", 1, {}); + cmpProductionQueue.AddBatch("units/test_b", "unit", 1, {}); + TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test_a"); + TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[1].unitTemplate, "units/test_b"); + + // Add a modifier that replaces unit A with unit C, + // adds a unit D and removes unit B from the roster. + Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => { + return HandleTokens(val, "units/{civ}_a>units/{civ}_c units/{civ}_d -units/{civ}_b"); + }); + + cmpProductionQueue.OnValueModification({ + "component": "ProductionQueue", + "valueNames": ["ProductionQueue/Entities/_string"], + "entities": [ent] + }); + + TS_ASSERT_UNEVAL_EQUALS( + cmpProductionQueue.GetEntitiesList(), ["units/test_c", "units/test_d"] + ); + TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test_c"); + TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue().length, 1); +} + +testEntitiesList(); regression_test_d1879(); +test_token_changes();