diff --git a/binaries/data/mods/public/globalscripts/Technologies.js b/binaries/data/mods/public/globalscripts/Technologies.js index 3701dbf209..a1f6187014 100644 --- a/binaries/data/mods/public/globalscripts/Technologies.js +++ b/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 @@ function GetTechModifiedProperty(modifications, classes, originalValue) 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 @@ function GetTechModifiedProperty(modifications, classes, originalValue) 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; } /** @@ -47,6 +48,35 @@ function DoesModificationApply(modification, classes) return MatchesClassList(classes, modification.affects); } +/** + * 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. diff --git a/binaries/data/mods/public/gui/session/messages.js b/binaries/data/mods/public/gui/session/messages.js index 7422ccc875..30d666bc8b 100644 --- a/binaries/data/mods/public/gui/session/messages.js +++ b/binaries/data/mods/public/gui/session/messages.js @@ -214,12 +214,6 @@ var g_NotificationsTypes = 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 diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js index 6016980f30..9b545b7ac6 100644 --- a/binaries/data/mods/public/gui/session/session.js +++ b/binaries/data/mods/public/gui/session/session.js @@ -620,6 +620,13 @@ function onSimulationUpdate() } g_SimState = undefined; + // Some changes may require re-rendering the selection. + if (Engine.GuiInterfaceCall("IsSelectionDirty")) + { + g_Selection.onChange(); + Engine.GuiInterfaceCall("ResetSelectionDirty"); + } + if (!GetSimState()) return; diff --git a/binaries/data/mods/public/simulation/ai/common-api/entity.js b/binaries/data/mods/public/simulation/ai/common-api/entity.js index c40a41734f..b6d3b1fedb 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ b/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -15,8 +15,7 @@ m.Template = m.Class({ 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; diff --git a/binaries/data/mods/public/simulation/ai/petra/buildManager.js b/binaries/data/mods/public/simulation/ai/petra/buildManager.js index 40469cf9bd..90774096fe 100644 --- a/binaries/data/mods/public/simulation/ai/petra/buildManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/buildManager.js @@ -79,6 +79,21 @@ PETRA.BuildManager.prototype.checkEvents = function(gameState, events) 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); + } }; diff --git a/binaries/data/mods/public/simulation/components/AIInterface.js b/binaries/data/mods/public/simulation/components/AIInterface.js index d05992bf98..db3bbd7235 100644 --- a/binaries/data/mods/public/simulation/components/AIInterface.js +++ b/binaries/data/mods/public/simulation/components/AIInterface.js @@ -14,6 +14,7 @@ AIInterface.prototype.EventNames = [ "AIMetadata", "PlayerDefeated", "EntityRenamed", + "ValueModification", "OwnershipChanged", "Garrison", "UnGarrison", @@ -251,7 +252,7 @@ AIInterface.prototype.OnTemplateModification = function(msg) 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.OnTemplateModification = function(msg) 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 @@ AIInterface.prototype.OnGlobalValueModification = function(msg) 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" || diff --git a/binaries/data/mods/public/simulation/components/Builder.js b/binaries/data/mods/public/simulation/components/Builder.js index 2f4f92e3e8..297cb8fd5c 100644 --- a/binaries/data/mods/public/simulation/components/Builder.js +++ b/binaries/data/mods/public/simulation/components/Builder.js @@ -34,6 +34,8 @@ Builder.prototype.GetEntitiesList = function() 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.PerformBuilding = function(target) } }; +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); diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index 783c08b096..cbec6134ab 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -35,6 +35,7 @@ GuiInterface.prototype.Init = function() this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; this.templateModified = {}; + this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; @@ -659,6 +660,7 @@ GuiInterface.prototype.GetNeededResources = function(player, data) GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; + this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) @@ -671,6 +673,35 @@ GuiInterface.prototype.ResetTemplateModified = function() this.templateModified = {}; }; +/** + * 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 @@ -1991,7 +2022,9 @@ let exposedFunctions = { "GetTraderNumber": 1, "GetTradingGoods": 1, "IsTemplateModified": 1, - "ResetTemplateModified": 1 + "ResetTemplateModified": 1, + "IsSelectionDirty": 1, + "ResetSelectionDirty": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) diff --git a/binaries/data/mods/public/simulation/components/Player.js b/binaries/data/mods/public/simulation/components/Player.js index f62bdd7f1b..7935ab6d1d 100644 --- a/binaries/data/mods/public/simulation/components/Player.js +++ b/binaries/data/mods/public/simulation/components/Player.js @@ -863,23 +863,12 @@ Player.prototype.AddDisabledTemplate = function(template) { 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 @@ Player.prototype.SetDisabledTemplates = function(templates) 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() diff --git a/binaries/data/mods/public/simulation/components/ProductionQueue.js b/binaries/data/mods/public/simulation/components/ProductionQueue.js index 244629a990..f2bdb8a017 100644 --- a/binaries/data/mods/public/simulation/components/ProductionQueue.js +++ b/binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -75,60 +75,122 @@ ProductionQueue.prototype.Init = function() */ 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+/); + let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {}; - // Filter out disabled and invalid entities. - let disabledEntities = cmpPlayer.GetDisabledTemplates(); - entitiesList = entitiesList.filter(ent => !disabledEntities[ent] && cmpTemplateManager.TemplateExists(ent)); + /** + * 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). + */ - // 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; + 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); - for (let item of this.queue) - if (item.unitTemplate) - item.unitTemplate = upgradeTemplate(item.unitTemplate); + 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()); +}; + +/* + * 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 @@ ProductionQueue.prototype.GetTechnologiesList = function() return []; let string = this.template.Technologies._string; + string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity); + if (!string) return []; @@ -148,8 +212,7 @@ ProductionQueue.prototype.GetTechnologiesList = function() 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 @@ ProductionQueue.prototype.OnOwnershipChanged = function(msg) 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.OnOwnershipChanged = function(msg) ProductionQueue.prototype.OnCivChanged = function() { - this.CalculateEntitiesList(); + this.CalculateEntitiesMap(); }; ProductionQueue.prototype.OnDestroy = function() @@ -866,15 +929,29 @@ ProductionQueue.prototype.OnValueModification = function(msg) // 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); diff --git a/binaries/data/mods/public/simulation/components/tests/test_Builder.js b/binaries/data/mods/public/simulation/components/tests/test_Builder.js index 1051bf19cf..dfd6b44ce7 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Builder.js +++ b/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 @@ AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "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" } diff --git a/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js b/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js index 8749457932..0a324ea0d0 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js +++ b/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js @@ -1,145 +1,7 @@ -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"); - -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 => ({}) -}); - -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.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), []); - -AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEntityID -}); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({}), - "GetPlayerID": () => playerId -}); - -AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, - "IsInProgress": () => false, - "IsTechnologyResearched": () => false -}); - -AddMock(productionQueueId, IID_Ownership, { - "GetOwner": () => playerId -}); - -AddMock(productionQueueId, IID_Identity, { - "GetCiv": () => "iber" -}); - -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 => ({}) -}); - -cmpProductionQueue.CalculateEntitiesList(); -TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber_support_female_citizen"]); - -AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({}) -}); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }), - "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"] -); - -AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/iber_infantry_swordsman_b": true }), - "GetPlayerID": () => playerId -}); - -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.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(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetPlayerID": () => playerId -}); -TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] -); - Engine.LoadComponentScript("interfaces/BuildRestrictions.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); @@ -148,12 +10,150 @@ Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); Engine.LoadComponentScript("interfaces/Trigger.js"); Engine.LoadComponentScript("EntityLimits.js"); + +Engine.RegisterGlobal("Resources", { + "BuildSchema": (a, b) => {} +}); +Engine.LoadComponentScript("ProductionQueue.js"); Engine.LoadComponentScript("TrainingRestrictions.js"); -Engine.LoadHelperScript("Sound.js"); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value); +function testEntitiesList() +{ + Engine.RegisterGlobal("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 => ({}) + }); + + 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(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => playerEntityID + }); + + AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}), + "GetDisabledTemplates": () => ({}), + "GetPlayerID": () => playerId + }); + + AddMock(playerEntityID, IID_TechnologyManager, { + "CheckTechnologyRequirements": () => true, + "IsInProgress": () => false, + "IsTechnologyResearched": () => false + }); + + AddMock(productionQueueId, IID_Ownership, { + "GetOwner": () => playerId + }); + + AddMock(productionQueueId, IID_Identity, { + "GetCiv": () => "iber" + }); + + 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"] + ); + + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": name => name == "units/iber_support_female_citizen", + "GetTemplate": name => ({}) + }); + + cmpProductionQueue.CalculateEntitiesMap(); + TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber_support_female_citizen"]); + + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "GetTemplate": name => ({}) + }); + + 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"] + ); + + 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() { // Setup @@ -280,4 +280,74 @@ function regression_test_d1879() 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();