Index: binaries/data/mods/public/globalscripts/ModificationTemplates.js =================================================================== --- binaries/data/mods/public/globalscripts/ModificationTemplates.js +++ binaries/data/mods/public/globalscripts/ModificationTemplates.js @@ -17,11 +17,6 @@ deepfreeze(this.templates); } -ModificationTemplates.prototype.GetNames = function() -{ - return this.names; -}; - ModificationTemplates.prototype.Has = function(name) { return this.names.indexOf(name) != -1; @@ -37,11 +32,9 @@ return this.templates; }; - function LoadModificationTemplates() { global.AuraTemplates = new ModificationTemplates("simulation/data/auras/"); - global.TechnologyTemplates = new ModificationTemplates("simulation/data/technologies/"); } /** @@ -125,12 +118,14 @@ "" + "" + "" + - "" + - "" + - "tokens" + - "" + - "" + - "" + + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + + "" + "" + "" + "" + @@ -168,7 +163,8 @@ if (template.Multiply) effect.multiply = +template.Multiply; if (template.Replace) - effect.replace = template.Replace; + effect.replace = isNaN(template.Replace) ? template.Replace : +template.Replace; + effect.affects = template.Affects ? template.Affects._string.split(/\s/) : []; let ret = {}; Index: binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- binaries/data/mods/public/globalscripts/Technologies.js +++ binaries/data/mods/public/globalscripts/Technologies.js @@ -367,27 +367,21 @@ * @param {Object} phases - The current available store of phases. * @return {array} List of phases */ -function UnravelPhases(phases) +function UnravelPhases(phases, civCode) { - let phaseMap = {}; - for (let phaseName in phases) + const phaseMap = {}; + for (const phaseName in phases) { - let phaseData = phases[phaseName]; - if (!phaseData.reqs.length || !phaseData.reqs[0].techs || !phaseData.replaces) - continue; + const phaseData = phases[phaseName]; + + const prevPhase = phaseData.technology.supersedes.replace(/\{(civ|native)\}/g, civCode); + phaseMap[phaseName] = prevPhase; - let myPhase = phaseData.replaces[0]; - let reqPhase = phaseData.reqs[0].techs[0]; - if (phases[reqPhase] && phases[reqPhase].replaces) - reqPhase = phases[reqPhase].replaces[0]; - - phaseMap[myPhase] = reqPhase; - if (!phaseMap[reqPhase]) - phaseMap[reqPhase] = undefined; + if (!phaseMap[prevPhase]) + phaseMap[prevPhase] = undefined; } let phaseList = Object.keys(phaseMap); - phaseList.sort((a, b) => phaseList.indexOf(a) - phaseList.indexOf(phaseMap[b])); - + phaseList.sort((a, b) => a == phaseMap[b] ? -1 : 1); return phaseList; } Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -313,8 +313,12 @@ if (template.Cost) { ret.cost = {}; - for (let resCode in template.Cost.Resources) - ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); + if (template.Cost.Resources) + { + ret.cost.resources = {} + for (const resCode in template.Cost.Resources) + ret.cost.resources[resCode] = getEntityValue("Cost/Resources/" + resCode); + } if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); @@ -494,6 +498,14 @@ "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; + if (template.Technology) + ret.technology = { + "choice": template.Technology.Choice, + "choiceRoot": template.Technology.ChoiceRoot, + "modifications": template.Technology.Modifiers, + "supersedes": template.Technology.Supersedes + }; + if (template.Treasure) { ret.treasure = { @@ -551,49 +563,6 @@ } /** - * Get basic information about a technology template. - * @param {Object} template - A valid template as obtained by loading the tech JSON file. - * @param {string} civ - Civilization for which the tech requirements should be calculated. - */ -function GetTechnologyBasicDataHelper(template, civ) -{ - return { - "name": { - "generic": template.genericName - }, - "icon": template.icon ? "technologies/" + template.icon : undefined, - "description": template.description, - "reqs": DeriveTechnologyRequirements(template, civ), - "modifications": template.modifications, - "affects": template.affects, - "replaces": template.replaces - }; -} - -/** - * Get information about a technology template. - * @param {Object} template - A valid template as obtained by loading the tech JSON file. - * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. - * @param {Object} resources - An instance of the Resources class. - */ -function GetTechnologyDataHelper(template, civ, resources) -{ - let ret = GetTechnologyBasicDataHelper(template, civ); - - if (template.specificName) - ret.name.specific = template.specificName[civ] || template.specificName.generic; - - ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; - for (let type of resources.GetCodes()) - ret.cost[type] = +(template.cost && template.cost[type] || 0); - - ret.tooltip = template.tooltip; - ret.requirementsTooltip = template.requirementsTooltip || ""; - - return ret; -} - -/** * Get information about an aura template. * @param {object} template - A valid template as obtained by loading the aura JSON file. */ Index: binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- binaries/data/mods/public/gui/common/tooltips.js +++ binaries/data/mods/public/gui/common/tooltips.js @@ -710,7 +710,9 @@ { let totalCosts = {}; for (let r of getCostTypes()) - if (template.cost[r]) + if (template.cost.resources && template.cost.resources[r]) + totalCosts[r] = Math.floor(template.cost.resources[r] * trainNum); + else if (template.cost[r]) totalCosts[r] = Math.floor(template.cost[r] * trainNum); return totalCosts; @@ -962,19 +964,67 @@ return ""; } -function getRequirementsTooltip(enabled, requirements, civ) +function getRequirementsTooltip(enabled, requirements, civ, playerID) { if (enabled) return ""; - // Simple requirements (one tech) can be translated on the fly. + // Simple requirements can be translated on the fly... if ("Techs" in requirements && !requirements.Techs._string.includes(" ") && requirements.Techs._string[0] != "!") return objectionFont(sprintf(translate("Requires %(technology)s"), { - "technology": getEntityNames(GetTechnologyData(requirements.Techs._string, civ)) + "technology": getEntityNames(GetTemplateData(requirements.Techs._string.replace("{civ}", civ), playerID)) })); - // More complex ones need a tooltip. + if ("Entities" in requirements) + { + const playerState = GetSimState().players[playerID]; + const entityCounts = []; + for (const entity in requirements.Entities) + { + const req = requirements.Entities[entity]; + let needed = 0; + let current = 0; + if ("Count" in req) + { + needed = +req.Count; + current = playerState.classCounts[entity] || 0; + } + else if ("Variants" in req) + { + needed = +req.Variants; + current = playerState.typeCountsByClass[entity] ? + Object.keys(playerState.typeCountsByClass[entity]).length : 0; + } + else + { + warn("Unknown entity requirement: " + uneval(requirements) + "."); + return ""; + } + + const remaining = needed - current; + if (remaining < 1) + continue; + + if (current === 0) + entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), { + "number": remaining, + "class": translate(entity) + })); + else + entityCounts.push(sprintf(translatePlural("%(number)s more entity of class %(class)s", "%(number)s more entities of class %(class)s", remaining), { + "number": remaining, + "class": translate(entity) + })); + } + + const tip = sprintf(translate("Requires %(entityCounts)s"), { + "entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", ")) + }); + return objectionFont(tip); + } + + // ...more complex ones need a tooltip. if ("Tooltip" in requirements) return objectionFont(translate(requirements.Tooltip)); Index: binaries/data/mods/public/gui/reference/common/TemplateLoader.js =================================================================== --- binaries/data/mods/public/gui/reference/common/TemplateLoader.js +++ binaries/data/mods/public/gui/reference/common/TemplateLoader.js @@ -100,23 +100,17 @@ * * Loads from local cache if available, else from file system. * + * @param {string} civCode * @param {string} templateName * @return {Object} Object containing raw template data. */ - loadTechnologyTemplate(templateName) + loadTechnologyTemplate(templateName, civCode) { if (!(templateName in this.technologyData)) { - let data = Engine.ReadJSONFile(this.TechnologyPath + templateName + ".json"); + const data = clone(Engine.GetTemplate(templateName)); translateObjectKeys(data, this.TechnologyTranslateKeys); - // Translate specificName as in GetTechnologyData() from gui/session/session.js - if (typeof (data.specificName) === 'object') - for (let civ in data.specificName) - data.specificName[civ] = translate(data.specificName[civ]); - else if (data.specificName) - warn("specificName should be an object of civ->name mappings in " + templateName + ".json"); - this.technologyData[templateName] = data; } @@ -162,13 +156,10 @@ }; if (template.Researcher?.Technologies?._string) - for (let technologyName of template.Researcher.Technologies._string.split(" ")) + for (let technologyName of template.Researcher.Technologies._string.replace(/\{(civ|native)\}/g, civCode).split(" ")) { - if (technologyName.indexOf("{civ}") != -1) - { - const civTechName = technologyName.replace("{civ}", civCode); - technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic"); - } + if (!Engine.TemplateExists(technologyName)) + continue; if (this.isPairTech(technologyName)) { @@ -205,7 +196,7 @@ { const modificationData = []; for (const techName of this.autoResearchTechList) - modificationData.push(GetTechnologyBasicDataHelper(this.loadTechnologyTemplate(techName), civCode)); + modificationData.push(GetTemplateDataHelper(this.loadTechnologyTemplate(techName), civCode)); for (const auraName of auraList) modificationData.push(this.loadAuraTemplate(auraName)); @@ -232,21 +223,15 @@ } /** - * Crudely iterates through every tech JSON file and identifies those + * Crudely iterates through every tech file and identifies those * that are auto-researched. * * @return {array} List of techs that are researched automatically */ findAllAutoResearchedTechs() { - let techList = []; - for (let templateName of listFiles(this.TechnologyPath, ".json", true)) - { - let data = this.loadTechnologyTemplate(templateName); - if (data && data.autoResearch) - techList.push(templateName); - } - return techList; + return Engine.FindAllTemplates().filter(templateName => + this.loadTechnologyTemplate(templateName)?.Technology?.AutoResearched); } /** @@ -333,4 +318,4 @@ */ TemplateLoader.prototype.AuraTranslateKeys = ["auraName", "auraDescription"]; TemplateLoader.prototype.EntityTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"]; -TemplateLoader.prototype.TechnologyTranslateKeys = ["genericName", "tooltip", "description"]; +TemplateLoader.prototype.TechnologyTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"]; Index: binaries/data/mods/public/gui/reference/common/TemplateParser.js =================================================================== --- binaries/data/mods/public/gui/reference/common/TemplateParser.js +++ binaries/data/mods/public/gui/reference/common/TemplateParser.js @@ -77,14 +77,13 @@ else { let highestPhaseIndex = 0; - for (const tech of parsed.requirements.Techs._string.split(" ")) + for (const tech of parsed.requirements.Techs._string.replace(/\{(civ|native)\}/g, civCode).split(" ")) { if (tech[0] === "!") continue; const phaseIndex = this.phaseList.indexOf( - this.TemplateLoader.isPhaseTech(tech) ? this.getActualPhase(tech) : - this.getPhaseOfTechnology(tech, civCode)); + this.getPhaseOfTechnology(tech, civCode)); if (phaseIndex > highestPhaseIndex) highestPhaseIndex = phaseIndex; } @@ -173,7 +172,7 @@ } /** - * Load and parse technology from json template. + * Load and parse technology from template. * * @param {string} technologyName * @param {string} civCode @@ -181,10 +180,11 @@ */ getTechnology(technologyName, civCode) { - if (!TechnologyTemplateExists(technologyName)) + technologyName = technologyName.replace(/\{(civ|native)\}/g, civCode); + if (!Engine.TemplateExists(technologyName)) return null; - if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases) + if (technologyName in this.phases) return this.phases[technologyName]; if (!(civCode in this.techs)) @@ -192,8 +192,8 @@ else if (technologyName in this.techs[civCode]) return this.techs[civCode][technologyName]; - let template = this.TemplateLoader.loadTechnologyTemplate(technologyName); - const tech = GetTechnologyDataHelper(template, civCode, g_ResourceData, this.modifiers[civCode] || {}); + let template = this.TemplateLoader.loadTechnologyTemplate(technologyName, civCode); + const tech = GetTemplateDataHelper(template, null, g_ResourceData, this.modifiers[civCode] || {}); tech.name.internal = technologyName; if (template.pair !== undefined) @@ -203,12 +203,7 @@ } if (this.TemplateLoader.isPhaseTech(technologyName)) - { - tech.actualPhase = technologyName; - if (tech.replaces !== undefined) - tech.actualPhase = tech.replaces[0]; this.phases[technologyName] = tech; - } else this.techs[civCode][technologyName] = tech; return tech; @@ -282,8 +277,7 @@ continue; const phaseIndex = this.phaseList.indexOf( - this.TemplateLoader.isPhaseTech(tech) ? this.getActualPhase(tech) : - this.getPhaseOfTechnology(tech, civCode)); + this.getPhaseOfTechnology(tech, civCode)); if (phaseIndex > highestPhaseIndex) highestPhaseIndex = phaseIndex; } @@ -308,51 +302,29 @@ getPhaseOfTechnology(techName, civCode) { let phaseIdx = -1; - - if (basename(techName).startsWith("phase")) + if (basename(techName).includes("phase")) { - if (!this.phases[techName].reqs) - return false; - - phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName)); + phaseIdx = this.phaseList.indexOf(techName); if (phaseIdx > 0) return this.phaseList[phaseIdx - 1]; } - let techReqs = this.getTechnology(techName, civCode).reqs; + let techReqs = this.getTechnology(techName, civCode).requirements; if (!techReqs) return false; - for (let option of techReqs) - if (option.techs) - for (let tech of option.techs) - { - if (basename(tech).startsWith("phase")) - return tech; - if (basename(tech).startsWith("pair")) - continue; - phaseIdx = Math.max(phaseIdx, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode))); - } + if (techReqs.Techs?._string) + for (let tech of techReqs.Techs._string.replace(/\{(civ|native)\}/g, civCode).split(" ")) + { + if (basename(tech).includes("phase")) + return tech; + if (basename(tech).startsWith("pair")) + continue; + phaseIdx = Math.max(phaseIdx, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode))); + } return this.phaseList[phaseIdx] || false; } - /** - * Returns the actual phase a certain phase tech represents or stands in for. - * - * For example, passing `phase_city_athen` would result in `phase_city`. - * - * @param {string} phaseName - * @return {string} - */ - getActualPhase(phaseName) - { - if (this.phases[phaseName]) - return this.phases[phaseName].actualPhase; - - warn("Unrecognized phase (" + phaseName + ")"); - return this.phaseList[0]; - } - getModifiers(civCode) { return this.modifiers[civCode]; @@ -376,7 +348,7 @@ if (this.TemplateLoader.isPhaseTech(techcode)) this.getTechnology(techcode, civCode); - this.phaseList = UnravelPhases(this.phases); + this.phaseList = UnravelPhases(this.phases, civCode); // Make sure all required generic phases are loaded and parsed for (let phasecode of this.phaseList) Index: binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdent.js =================================================================== --- binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdent.js +++ binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdent.js @@ -50,7 +50,7 @@ drawPhaseIcon(phaseIcon, phaseIndex, civCode) { let phaseName = this.page.TemplateParser.phaseList[phaseIndex]; - let prodPhaseTemplate = this.page.TemplateParser.getTechnology(phaseName + "_" + civCode, civCode) || this.page.TemplateParser.getTechnology(phaseName, civCode); + let prodPhaseTemplate = this.page.TemplateParser.getTechnology(phaseName, civCode); phaseIcon.sprite = "stretched:" + this.page.IconPath + prodPhaseTemplate.icon; phaseIcon.tooltip = getEntityNamesFormatted(prodPhaseTemplate); Index: binaries/data/mods/public/gui/session/ResearchProgress.js =================================================================== --- binaries/data/mods/public/gui/session/ResearchProgress.js +++ binaries/data/mods/public/gui/session/ResearchProgress.js @@ -68,7 +68,7 @@ { this.researcher = researchStatus.researcher; - let template = GetTechnologyData(techName, g_Players[g_ViewedPlayer].civ); + const template = GetTemplateData(techName, g_ViewedPlayer); let modifier = "stretched:"; if (researchStatus.paused) modifier += "color:0 0 0 127:grayscale:"; Index: binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js =================================================================== --- binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js +++ binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js @@ -116,7 +116,7 @@ return { "text": sprintf(message, { "player": colorizePlayernameByID(msg.player), - "phaseName": getEntityNames(GetTechnologyData(msg.phaseName, g_Players[msg.player].civ)) + "phaseName": getEntityNames(GetTemplateData(msg.phaseName, msg.player)) }) }; } Index: binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js =================================================================== --- binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js +++ binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js @@ -74,7 +74,9 @@ tooltip += "\n" + getRequirementsTooltip( false, template.requirements, - GetSimState().players[g_ViewedPlayer].civ); + GetSimState().players[g_ViewedPlayer].civ, + g_ViewedPlayer + ); this.diplomacySpyRequest.enabled = false; this.diplomacySpyRequest.tooltip = tooltip; Index: binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- binaries/data/mods/public/gui/session/selection_details.js +++ binaries/data/mods/public/gui/session/selection_details.js @@ -80,7 +80,7 @@ // Rank if (entState.identity && entState.identity.rank && entState.identity.classes) { - const rankObj = GetTechnologyData(entState.identity.rankTechName, playerState.civ); + const rankObj = GetTemplateData(entState.identity.rankTechName, entState.player); Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), { "rank": translateWithContext("Rank", entState.identity.rank) }) + (rankObj ? "\n" + rankObj.tooltip : ""); Index: binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels.js +++ binaries/data/mods/public/gui/session/selection_panels.js @@ -203,7 +203,7 @@ tooltips.push( formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), - getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ), + getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ, data.player), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); @@ -547,7 +547,7 @@ if (queuedItem.unitTemplate) template = GetTemplateData(queuedItem.unitTemplate); else if (queuedItem.technologyTemplate) - template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ); + template = GetTemplateData(queuedItem.technologyTemplate); else { warning("Unknown production queue template " + uneval(queuedItem)); @@ -665,16 +665,15 @@ tech => tech != null && !ret.some( item => (item.tech == tech || - item.tech.pair && - tech.pair && - item.tech.bottom == tech.bottom && - item.tech.top == tech.top) && + item.tech.ChoiceRoot && + tech.ChoiceRoot && + item.tech.Choices == tech.Choices) && Object.keys(item.techCostMultiplier).every( k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k]) )); if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() && - getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2)) + getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.ChoiceRoot) ? 1 : 2)) ret = ret.concat(filteredTechs.map(tech => ({ "tech": tech, "techCostMultiplier": state.researcher.techCostMultiplier, @@ -703,7 +702,7 @@ let position = data.i + data.rowLength; // Only show the top button for pairs - if (!data.item.tech.pair) + if (!data.item.tech.ChoiceRoot) Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; // Set up the tech connector @@ -712,35 +711,32 @@ setPanelObjectPosition(pair, data.i, data.rowLength); // Handle one or two techs (tech pair) - let player = data.player; - let playerState = GetSimState().players[player]; - for (let tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech]) + const player = data.player; + const playerState = GetSimState().players[player]; + for (const tech of data.item.tech.Choices ? data.item.tech.Choices : [data.item.tech]) { - // Don't change the object returned by GetTechnologyData - let template = clone(GetTechnologyData(tech, playerState.civ)); - if (!template) - return false; - + const template = clone(GetTemplateData(tech)); // Not allowed by civ. - if (!template.reqs) + if (!template) { - // One of the pair may still be researchable by the current civ, + // One of the choices may still be researchable by the current civ, // hence don't hide everything. Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; pair.hidden = true; continue; } - for (let res in template.cost) - template.cost[res] *= data.item.techCostMultiplier[res] !== undefined ? data.item.techCostMultiplier[res] : 1; + template.cost.time *= data.item.techCostMultiplier.time ?? 1; + for (const res in template.cost.resources) + template.cost.resources[res] *= data.item.techCostMultiplier[res] ?? 1; - let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { - "cost": template.cost, + const neededResources = Engine.GuiInterfaceCall("GetNeededResources", { + "cost": template.cost.resources, "player": player }); - let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", { - "tech": tech, + const requirementsPassed = Engine.GuiInterfaceCall("AreRequirementsMet", { + "requirements": template.requirements, "player": player }); @@ -754,47 +750,8 @@ showTemplateViewerOnRightClickTooltip ].map(func => func(template)); - if (!requirementsPassed) - { - let tip = template.requirementsTooltip; - let reqs = template.reqs; - for (let req of reqs) - { - if (!req.entities) - continue; - - let entityCounts = []; - for (let entity of req.entities) - { - let current = 0; - switch (entity.check) - { - case "count": - current = playerState.classCounts[entity.class] || 0; - break; - - case "variants": - current = playerState.typeCountsByClass[entity.class] ? - Object.keys(playerState.typeCountsByClass[entity.class]).length : 0; - break; - } - - let remaining = entity.number - current; - if (remaining < 1) - continue; - - entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), { - "number": remaining, - "class": translate(entity.class) - })); - } - - tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), { - "entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", ")) - }); - } - tooltips.push(objectionFont(tip)); - } + tooltips.push(getRequirementsTooltip(requirementsPassed, template.requirements, playerState.civ, player)); + tooltips.push(getNeededResourcesTooltip(neededResources)); button.tooltip = tooltips.filter(tip => tip).join("\n"); @@ -802,7 +759,7 @@ addResearchToQueue(data.item.researchFacilityId, t); })(tech); - let showTemplateFunc = (t => function() { + const showTemplateFunc = (t => function() { showTemplateDetails( t, GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv); @@ -811,10 +768,10 @@ button.onPressRight = showTemplateFunc(tech); button.onPressRightDisabled = showTemplateFunc(tech); - if (data.item.tech.pair) + if (data.item.tech.Choices) { // On mouse enter, show a cross over the other icon - let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]"); + const unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]"); button.onMouseEnter = function() { unchosenIcon.hidden = false; }; @@ -1050,7 +1007,7 @@ tooltips.push(showTemplateViewerOnRightClickTooltip()); tooltips.push( formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch), - getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ), + getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ, data.player), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); @@ -1167,7 +1124,7 @@ getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length), formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), - getRequirementsTooltip(requirementsMet, data.item.requirements, GetSimState().players[data.player].civ), + getRequirementsTooltip(requirementsMet, data.item.requirements, GetSimState().players[data.player].civ, data.player), getNeededResourcesTooltip(neededResources), showTemplateViewerOnRightClickTooltip()); Index: binaries/data/mods/public/gui/session/session.js =================================================================== --- binaries/data/mods/public/gui/session/session.js +++ binaries/data/mods/public/gui/session/session.js @@ -221,30 +221,12 @@ if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": player }); - translateObjectKeys(template, ["specific", "generic", "tooltip"]); + translateObjectKeys(template, ["specific", "generic", "tooltip", "history"]); g_TemplateData[templateName] = deepfreeze(template); } return g_TemplateData[templateName]; } -function GetTechnologyData(technologyName, civ) -{ - if (!g_TechnologyData[civ]) - g_TechnologyData[civ] = {}; - - if (!(technologyName in g_TechnologyData[civ])) - { - const tech = TechnologyTemplates.Get(technologyName); - if (!tech) - return; - let template = GetTechnologyDataHelper(tech, civ, g_ResourceData); - translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); - g_TechnologyData[civ][technologyName] = deepfreeze(template); - } - - return g_TechnologyData[civ][technologyName]; -} - function init(initData, hotloadData) { if (!g_Settings) Index: binaries/data/mods/public/simulation/components/EntityLimits.js =================================================================== --- binaries/data/mods/public/simulation/components/EntityLimits.js +++ binaries/data/mods/public/simulation/components/EntityLimits.js @@ -75,30 +75,6 @@ this.classCount = {}; this.removedLimit = {}; this.matchTemplateCount = {}; - for (var category in this.template.Limits) - { - this.limit[category] = +this.template.Limits[category]; - this.count[category] = 0; - if (category in this.template.LimitChangers) - { - this.changers[category] = {}; - for (var c in this.template.LimitChangers[category]) - this.changers[category][c] = +this.template.LimitChangers[category][c]; - } - if (category in this.template.LimitRemovers) - { - // Keep a copy of removable limits for possible restoration. - this.removedLimit[category] = this.limit[category]; - this.removers[category] = {}; - for (var c in this.template.LimitRemovers[category]) - { - this.removers[category][c] = this.template.LimitRemovers[category][c]._string.split(/\s+/); - if (c === "RequiredClasses") - for (var cls of this.removers[category][c]) - this.classCount[cls] = 0; - } - } - } }; EntityLimits.prototype.ChangeCount = function(category, value) @@ -298,4 +274,33 @@ this.UpdateLimitRemoval(); }; +EntityLimits.prototype.OnCreate = function(msg) +{ + const civ = Engine.QueryInterface(this.entity, IID_Identity).GetCiv(); + for (const category in this.template.Limits) + { + this.limit[category] = +this.template.Limits[category]; + this.count[category] = 0; + if (category in this.template.LimitChangers) + { + this.changers[category] = {}; + for (const c in this.template.LimitChangers[category]) + this.changers[category][c] = +this.template.LimitChangers[category][c]; + } + if (category in this.template.LimitRemovers) + { + // Keep a copy of removable limits for possible restoration. + this.removedLimit[category] = this.limit[category]; + this.removers[category] = {}; + for (const c in this.template.LimitRemovers[category]) + { + this.removers[category][c] = this.template.LimitRemovers[category][c]._string.replace(/{civ}/gi, civ).split(/\s+/); + if (c === "RequiredClasses") + for (const cls of this.removers[category][c]) + this.classCount[cls] = 0; + } + } + } +}; + Engine.RegisterComponentType(IID_EntityLimits, "EntityLimits", EntityLimits); 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 @@ -672,18 +672,6 @@ }; /** - * Checks whether the requirements for this technology have been met. - */ -GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) -{ - let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); - if (!cmpTechnologyManager) - return false; - - return cmpTechnologyManager.CanResearch(data.tech); -}; - -/** * Returns technologies that are being actively researched, along with * which entity is researching them and how far along the research is. */ @@ -2073,7 +2061,6 @@ "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "AreRequirementsMet": 1, - "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, Index: binaries/data/mods/public/simulation/components/Researcher.js =================================================================== --- binaries/data/mods/public/simulation/components/Researcher.js +++ binaries/data/mods/public/simulation/components/Researcher.js @@ -198,40 +198,29 @@ let techs = string.split(/\s+/); - // Replace the civ specific technologies. - const civ = Engine.QueryInterface(playerEnt, IID_Identity).GetCiv(); + const playerCiv = Engine.QueryInterface(playerEnt, IID_Identity).GetCiv(); + const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity).GetCiv(); for (let i = 0; i < techs.length; ++i) - { - const tech = techs[i]; - if (tech.indexOf("{civ}") == -1) - continue; - const civTech = tech.replace("{civ}", civ); - techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); - } - - // Remove any technologies that can't be researched by this civ. - techs = techs.filter(tech => - cmpTechnologyManager.CheckTechnologyRequirements( - DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), civ), - true)); + techs[i] = techs[i].replace("{native}", nativeCiv).replace("{civ}", playerCiv); const techList = []; const superseded = {}; const disabledTechnologies = cmpPlayer.GetDisabledTechnologies(); + const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // Add any top level technologies to an array which corresponds to the displayed icons. // Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }. for (const tech of techs) { - if (disabledTechnologies && disabledTechnologies[tech]) + if (!cmpTemplateManager.TemplateExists(tech) || disabledTechnologies && disabledTechnologies[tech]) continue; - const template = TechnologyTemplates.Get(tech); - if (!template.supersedes || techs.indexOf(template.supersedes) === -1) + const template = cmpTemplateManager.GetTemplate(tech).Technology; + if (!template?.Supersedes || !techs.includes(template.Supersedes)) techList.push(tech); else - superseded[template.supersedes] = tech; + superseded[template.Supersedes] = tech; } // Now make researched/in progress techs invisible. @@ -256,9 +245,9 @@ continue; } - const template = TechnologyTemplates.Get(tech); - if (template.top) - ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom }; + const template = cmpTemplateManager.GetTemplate(tech).Technology; + if (template?.Choices) + ret[i] = { "Choices": template.Choices._string.split(" ") }; else ret[i] = tech; } @@ -282,7 +271,7 @@ }; /** - * Checks whether we can research the given technology, minding paired techs. + * Checks whether we can research the given technology. */ Researcher.prototype.IsTechnologyResearchedOrInProgress = function(tech) { @@ -293,13 +282,6 @@ if (!cmpTechnologyManager) return false; - const template = TechnologyTemplates.Get(tech); - if (template.top) - return cmpTechnologyManager.IsTechnologyResearched(template.top) || - cmpTechnologyManager.IsInProgress(template.top) || - cmpTechnologyManager.IsTechnologyResearched(template.bottom) || - cmpTechnologyManager.IsInProgress(template.bottom); - return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech); }; @@ -311,8 +293,7 @@ Researcher.prototype.QueueTechnology = function(templateName, metadata) { if (!this.GetTechnologiesList().some(tech => - tech && (tech == templateName || - tech.pair && (tech.top == templateName || tech.bottom == templateName)))) + tech && (tech == templateName || tech.Choices?.includes(templateName)))) { error("This entity cannot research " + templateName + "."); return -1; Index: binaries/data/mods/public/simulation/components/Technology.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/Technology.js @@ -0,0 +1,27 @@ +function Technology() {} + +Technology.prototype.Schema = + "Specifies the effects of a technology." + + "" + + "technologies/{civ}/phase_village" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ModificationsSchema + + "" + + "" + + "" + + "" + + "" + + ""; + +Engine.RegisterComponentType(IID_Technology, "Technology", Technology); Index: binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- binaries/data/mods/public/simulation/components/TechnologyManager.js +++ binaries/data/mods/public/simulation/components/TechnologyManager.js @@ -1,7 +1,15 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = - ""; + "Handles technologies for a player." + + "" + + "" + + "" + + "" + + "tokens" + + "" + + "" + + ""; /** * This object represents a technology under research. @@ -23,14 +31,14 @@ */ TechnologyManager.prototype.Technology.prototype.Queue = function(techCostMultiplier) { - const template = TechnologyTemplates.Get(this.templateName); + const template = GetTemplateDataHelper(Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName)); if (!template) return false; this.resources = {}; - if (template.cost) - for (const res in template.cost) - this.resources[res] = Math.floor(techCostMultiplier[res] * template.cost[res]); + if (template.cost?.resources) + for (const res in template.cost.resources) + this.resources[res] = Math.floor(techCostMultiplier[res] * template.cost.resources[res]); // ToDo: Subtract resources here or in cmpResearcher? const cmpPlayer = Engine.QueryInterface(this.player, IID_Player); @@ -38,7 +46,10 @@ if (!cmpPlayer?.TrySubtractResources(this.resources)) return false; - const time = techCostMultiplier.time * (template.researchTime || 0) * 1000; + if (template.technology?.choiceRoot) + Engine.QueryInterface(this.player, IID_TechnologyManager).StopChoiceRoot(template.Technology.ChoiceRoot); + + const time = techCostMultiplier.time * (template.cost?.time || 0) * 1000; this.timeRemaining = time; this.timeTotal = time; @@ -58,6 +69,10 @@ cmpPlayer?.RefundResources(this.resources); delete this.resources; + const template = GetTemplateDataHelper(Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName)); + if (template.technology?.choiceRoot) + Engine.QueryInterface(this.player, IID_TechnologyManager).StopChoiceRoot(template.Technology.ChoiceRoot); + if (this.started && this.templateName.startsWith("phase")) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "phase", @@ -89,29 +104,25 @@ { this.finished = true; - const template = TechnologyTemplates.Get(this.templateName); - if (template.soundComplete) - Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher); + const technologyManager = Engine.QueryInterface(this.player, IID_TechnologyManager) + technologyManager.MarkTechnologyAsResearched(this.templateName); + + const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName); + + if (template.Technology?.ChoiceRoot) + technologyManager.FinishChoiceRoot(template.Technology.ChoiceRoot, this.researcher); - if (template.modifications) + if (template.Sound?.completed) + Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.Sound.completed, this.researcher); + + if (template.Technology?.Modifiers) { const cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); - cmpModifiersManager.AddModifiers("tech/" + this.templateName, DeriveModificationsFromTech(template), this.player); + cmpModifiersManager.AddModifiers("tech/" + this.templateName, DeriveModificationsFromXMLTemplate(template.Technology.Modifiers), this.player); } - const cmpEntityLimits = Engine.QueryInterface(this.player, IID_EntityLimits); - const cmpTechnologyManager = Engine.QueryInterface(this.player, IID_TechnologyManager); - if (template.replaces && template.replaces.length > 0) - for (const i of template.replaces) - { - cmpTechnologyManager.MarkTechnologyAsResearched(i); - cmpEntityLimits?.UpdateLimitsFromTech(i); - } - - cmpTechnologyManager.MarkTechnologyAsResearched(this.templateName); - // ToDo: Move to EntityLimits.js. - cmpEntityLimits?.UpdateLimitsFromTech(this.templateName); + Engine.QueryInterface(this.player, IID_EntityLimits)?.UpdateLimitsFromTech(this.templateName); const playerID = Engine.QueryInterface(this.player, IID_Player).GetPlayerID(); Engine.PostMessage(this.player, MT_ResearchFinished, { "player": playerID, "tech": this.templateName }); @@ -200,22 +211,29 @@ // Maps from technolgy name to the technology object. this.researchQueued = new Map(); + // Keeps track of the choice roots that have choices queued. + this.queuedChoiceRoots = new Set(); + this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. - this.unresearchedAutoResearchTechs = new Set(); - let allTechs = TechnologyTemplates.GetAll(); - for (let key in allTechs) - if (allTechs[key].autoResearch || allTechs[key].top) - this.unresearchedAutoResearchTechs.add(key); + this.unresearchedAutoResearchTechs; +}; + +TechnologyManager.prototype.OnCreate = function() +{ + const civCode = Engine.QueryInterface(this.entity, IID_Identity).GetCiv(); + const autoResearchedTemplate = this.template?.AutoResearched?._string?.replace(/\{(civ|native)\}/g, civCode)?.split(" "); + this.unresearchedAutoResearchTechs = new Set(autoResearchedTemplate || []); }; TechnologyManager.prototype.SerializableAttributes = [ "researchedTechs", "classCounts", + "queuedChoiceRoots", "typeCountsByClass", "unresearchedAutoResearchTechs" ]; @@ -257,11 +275,10 @@ // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { - for (let key of this.unresearchedAutoResearchTechs) + for (const key of this.unresearchedAutoResearchTechs) { - let tech = TechnologyTemplates.Get(key); - if ((tech.autoResearch && this.CanResearch(key)) || - (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) + const tech = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(key).Technology; + if (this.CanResearch(key)) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); @@ -273,9 +290,7 @@ // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function(templateName) { - var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - var template = cmpTempManager.GetTemplate(templateName); - + const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(templateName); if (template.Identity?.Requirements) return RequirementsHelper.AreRequirementsMet(template.Identity.Requirements, Engine.QueryInterface(this.entity, IID_Player).GetPlayerID()); // If there is no required technology then this entity can be produced @@ -284,87 +299,34 @@ TechnologyManager.prototype.IsTechnologyQueued = function(tech) { - return this.researchQueued.has(tech); + return this.researchQueued.has(tech.replace("{civ}", Engine.QueryInterface(this.entity, IID_Identity).GetCiv())); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { - return this.researchedTechs.has(tech); + return this.researchedTechs.has(tech.replace("{civ}", Engine.QueryInterface(this.entity, IID_Identity).GetCiv())); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { - let template = TechnologyTemplates.Get(tech); - - if (!template) + const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(tech); + if (!("Technology" in template)) { warn("Technology \"" + tech + "\" does not exist"); return false; } - if (template.top && this.IsInProgress(template.top) || - template.bottom && this.IsInProgress(template.bottom)) - return false; - - if (template.pair && !this.CanResearch(template.pair)) - return false; - - if (this.IsInProgress(tech)) - return false; - - if (this.IsTechnologyResearched(tech)) + if (template.Technology?.ChoiceRoot && !this.CanResearch(template.Technology.ChoiceRoot)) return false; - return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Identity).GetCiv())); -}; - -/** - * Private function for checking a set of requirements is met - * @param {Object} reqs - Technology requirements as derived from the technology template by globalscripts - * @param {boolean} civonly - True if only the civ requirement is to be checked - * - * @return true if the requirements pass, false otherwise - */ -TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) -{ - let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); - - if (!reqs) + if (this.IsInProgress(tech) || this.IsTechnologyResearched(tech)) return false; - if (civonly || !reqs.length) - return true; - - return reqs.some(req => { - return Object.keys(req).every(type => { - switch (type) - { - case "techs": - return req[type].every(this.IsTechnologyResearched, this); - - case "entities": - return req[type].every(this.DoesEntitySpecPass, this); - } - return false; - }); - }); -}; + if (template.Identity?.Requirements) + return RequirementsHelper.AreRequirementsMet(template.Identity.Requirements, Engine.QueryInterface(this.entity, IID_Player).GetPlayerID()); -TechnologyManager.prototype.DoesEntitySpecPass = function(entity) -{ - switch (entity.check) - { - case "count": - if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) - return false; - break; - - case "variants": - if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) - return false; - break; - } + // If there is no requirement then this technology can be researched. return true; }; @@ -447,6 +409,32 @@ }; /** + * @param {string} choiceRootTech - The tech to mark as queued. + */ +TechnologyManager.prototype.QueueChoiceRoot = function(choiceRootTech) +{ + this.queuedChoiceRoots.add(choiceRootTech); +}; + +/** + * @param {string} choiceRootTech - The tech to unmark as queued. + */ +TechnologyManager.prototype.StopChoiceRoot = function(choiceRootTech) +{ + this.queuedChoiceRoots.delete(choiceRootTech); +}; + +/** + * @param {string} choiceRootTech - The tech to research. + * @param {number} researcher - Optionally the entity to couple with the research. + */ +TechnologyManager.prototype.FinishChoiceRoot = function(choiceRootTech, researcher = INVALID_ENTITY) +{ + this.StopChoiceRoot(choiceRootTech); + this.ResearchTechnology(choiceRootTech, researcher); +}; + +/** * Marks a technology as being queued for research at the given entityID. * @param {string} tech - The technology to queue. * @param {number} researcher - The entity ID of the entity researching this technology. @@ -511,7 +499,7 @@ */ TechnologyManager.prototype.IsInProgress = function(tech) { - return this.researchQueued.has(tech); + return this.researchQueued.has(tech) || this.queuedChoiceRoots.has(tech); }; TechnologyManager.prototype.GetBasicInfoOfStartedTechs = function() Index: binaries/data/mods/public/simulation/components/interfaces/Technology.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/Technology.js @@ -0,0 +1 @@ +Engine.RegisterInterface("Technology"); Index: binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js +++ binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js @@ -24,11 +24,15 @@ AddMock(10, IID_Player, { "GetPlayerID": id => 1 }); +AddMock(10, IID_Identity, { + "GetCiv": id => "athen" +}); AddMock(SYSTEM_ENTITY, IID_GuiInterface, { "PushNotification": () => {} }); let cmpEntityLimits = ConstructComponent(10, "EntityLimits", template); +cmpEntityLimits.OnCreate(null); // Test getters TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); Index: binaries/data/mods/public/simulation/components/tests/test_Researcher.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Researcher.js +++ binaries/data/mods/public/simulation/components/tests/test_Researcher.js @@ -13,9 +13,11 @@ const playerEntityID = 11; const entityID = 21; -Engine.RegisterGlobal("TechnologyTemplates", { - "Has": name => name == "phase_town_athen" || name == "phase_city_athen", - "Get": () => ({}) +let techsPresent = []; + +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": name => techsPresent.includes(name), + "GetTemplate": () => ({ "Technology": {} }) }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { @@ -31,7 +33,6 @@ }); AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, "IsInProgress": () => false, "IsTechnologyResearched": () => false }); @@ -44,31 +45,39 @@ "GetCiv": () => "iber" }); +techsPresent = ["technologies/iber/phase_town", "technologies/iber/phase_city"]; let cmpResearcher = ConstructComponent(entityID, "Researcher", { - "Technologies": { "_string": "gather_fishing_net " + - "phase_town_{civ} " + - "phase_city_{civ}" } + "Technologies": { "_string": ["technologies/{native}/gather_fishing_net", + "technologies/{civ}/phase_town", + "technologies/{civ}/phase_city"].join(" ") } }); TS_ASSERT_UNEVAL_EQUALS( + cmpResearcher.GetTechnologiesList(), techsPresent +); + +techsPresent = ["technologies/iber/gather_fishing_net", "technologies/athen/phase_town", "technologies/athen/phase_city"]; + +TS_ASSERT_UNEVAL_EQUALS( cmpResearcher.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] + ["technologies/iber/gather_fishing_net"] ); AddMock(playerEntityID, IID_Player, { - "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }) + "GetDisabledTechnologies": () => ({ "technologies/iber/gather_fishing_net": true }) }); AddMock(playerEntityID, IID_Identity, { "GetCiv": () => "athen", }); -TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), ["phase_town_athen", "phase_city_athen"]); +TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), [ + "technologies/athen/phase_town", "technologies/athen/phase_city" +]); AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, "IsInProgress": () => false, - "IsTechnologyResearched": tech => tech == "phase_town_athen" + "IsTechnologyResearched": tech => tech == "technologies/athen/phase_town" }); -TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), [undefined, "phase_city_athen"]); +TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), [undefined, "technologies/athen/phase_city"]); AddMock(playerEntityID, IID_Player, { "GetDisabledTechnologies": () => ({}) @@ -76,29 +85,29 @@ AddMock(playerEntityID, IID_Identity, { "GetCiv": () => "iber", }); +techsPresent.push("technologies/iber/phase_town", "technologies/iber/phase_city"); TS_ASSERT_UNEVAL_EQUALS( cmpResearcher.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] + ["technologies/iber/gather_fishing_net", "technologies/iber/phase_town", "technologies/iber/phase_city"] ); +techsPresent.push("some_test"); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => typeof value === "string" ? value + " some_test": value); TS_ASSERT_UNEVAL_EQUALS( cmpResearcher.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic", "some_test"] + ["technologies/iber/gather_fishing_net", "technologies/iber/phase_town", "technologies/iber/phase_city", "some_test"] ); // Test Queuing a tech. -const queuedTech = "gather_fishing_net"; +const queuedTech = "technologies/iber/gather_fishing_net"; const cost = { "food": 10 }; -Engine.RegisterGlobal("TechnologyTemplates", { - "Has": () => true, - "Get": () => ({ - "cost": cost, - "researchTime": 1 - }) + +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": name => true, + "GetTemplate": () => ({ "Technology": {} }) }); const cmpPlayer = AddMock(playerEntityID, IID_Player, { @@ -110,7 +119,6 @@ "GetCiv": () => "iber", }); const techManager = AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, "IsInProgress": () => false, "IsTechnologyResearched": () => false, "QueuedResearch": (templateName, researcher, techCostMultiplier) => { Index: binaries/data/mods/public/simulation/components/tests/test_Technologies.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Technologies.js +++ binaries/data/mods/public/simulation/components/tests/test_Technologies.js @@ -53,11 +53,13 @@ Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => typeof value === "string" ? value + " some_test": value); const template = { + "Technology": {}, "name": "templateName" }; -Engine.RegisterGlobal("TechnologyTemplates", { - "GetAll": () => [], - "Get": (tech) => { +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "FindAllTemplates": () => [], + "GetTemplate": (tech) => { return template; } }); @@ -74,13 +76,18 @@ AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); +AddMock(researcherID, IID_Identity, { + "GetCiv": () => "gaia" +}); AddMock(playerEntityID, IID_Identity, { "GetCiv": () => "gaia" }); -template.cost = { - "food": 100 +template.Cost = { + "Resources": { + "food": 100 + }, + "BuildTime": 1.5 }; -template.researchTime = 1.5; const cmpPlayer = ConstructComponent(playerEntityID, "Player", { "SpyCostMultiplier": "1", @@ -101,6 +108,8 @@ let id = cmpResearcher.QueueTechnology(template.name); +cmpTechnologyManager.OnCreate(); + TS_ASSERT_EQUALS(spyPlayerResSub._called, 1); TS_ASSERT(!cmpTechnologyManager.IsTechnologyResearched(template.name)); TS_ASSERT(cmpTechnologyManager.IsInProgress(template.name)); Index: binaries/data/mods/public/simulation/components/tests/test_Technology.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_Technology.js @@ -0,0 +1,2 @@ +Engine.LoadComponentScript("interfaces/Technology.js"); +Engine.LoadComponentScript("Technology.js"); Index: binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js +++ binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js @@ -8,10 +8,12 @@ ConstructComponent(SYSTEM_ENTITY, "Trigger"); -const techTemplate = {}; -Engine.RegisterGlobal("TechnologyTemplates", { - "GetAll": () => [], - "Get": (tech) => { +const techTemplate = { + "Technology": {}, +}; +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "FindAllTemplates": () => [], + "GetTemplate": (tech) => { return techTemplate; } }); @@ -21,38 +23,36 @@ let cmpTechnologyManager = ConstructComponent(playerEntityID, "TechnologyManager", null); -// Test CheckTechnologyRequirements -const template = { "requirements": { "all": [{ "entity": { "class": "Village", "number": 5 } }, { "civ": "athen" }] } }; -cmpTechnologyManager.classCounts.Village = 2; -TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen")), false); -TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen"), true), true); -TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "maur"), true), false); -cmpTechnologyManager.classCounts.Village = 6; -TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen")), true); -TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "maur")), false); - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); const templateName = "template"; -techTemplate.cost = { - "food": 100 +techTemplate.Cost = { + "Resources": { + "food": 100 + } }; const cmpPlayer = AddMock(playerEntityID, IID_Player, { "GetPlayerID": () => playerID, "TrySubtractResources": (resources) => { - TS_ASSERT_UNEVAL_EQUALS(resources, techTemplate.cost); + TS_ASSERT_UNEVAL_EQUALS(resources, techTemplate.Cost.Resources); // Just have enough resources. return true; }, "RefundResources": (resources) => { - TS_ASSERT_UNEVAL_EQUALS(resources, techTemplate.cost); + TS_ASSERT_UNEVAL_EQUALS(resources, techTemplate.Cost.Resources); }, }); const spyCmpPlayerSubtract = new Spy(cmpPlayer, "TrySubtractResources"); const spyCmpPlayerRefund = new Spy(cmpPlayer, "RefundResources"); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "gaia" +}); + +cmpTechnologyManager.OnCreate(); + TS_ASSERT(cmpTechnologyManager.QueuedResearch(templateName, INVALID_ENTITY, { "food": 1 })); TS_ASSERT(cmpTechnologyManager.IsInProgress(templateName)); TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, 1); @@ -62,7 +62,7 @@ TS_ASSERT(!cmpTechnologyManager.IsInProgress(templateName)); TS_ASSERT_EQUALS(spyCmpPlayerRefund._called, 1); -techTemplate.researchTime = 2; +techTemplate.Cost.BuildTime = 2; TS_ASSERT(cmpTechnologyManager.QueuedResearch(templateName, INVALID_ENTITY, { "food": 1, "time": 1 })); TS_ASSERT_EQUALS(cmpTechnologyManager.Progress(templateName, 500), 500); @@ -79,6 +79,10 @@ }, }); +AddMock(playerEntityID, IID_Identity, { + "GetCiv": () => "iber", +}); + TS_ASSERT(!cmpTechnologyManager.IsTechnologyResearched(templateName)); TS_ASSERT_EQUALS(cmpTechnologyManager.Progress(templateName, 2000), 1500); TS_ASSERT(cmpTechnologyManager.IsTechnologyResearched(templateName)); Index: binaries/data/mods/public/simulation/components/tests/test_Trainer.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Trainer.js +++ binaries/data/mods/public/simulation/components/tests/test_Trainer.js @@ -191,7 +191,7 @@ }, "LimitChangers": {}, "LimitRemovers": {} -}); +}).OnCreate(null); // Test that we can't exceed the entity limit. TS_ASSERT_EQUALS(cmpTrainer.QueueBatch(queuedUnit, 1), -1); // And that in that case, the resources are not lost. @@ -205,7 +205,7 @@ }, "LimitChangers": {}, "LimitRemovers": {} -}); +}).OnCreate(null); let id = cmpTrainer.QueueBatch(queuedUnit, 1); TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, 2); TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1); Index: binaries/data/mods/public/simulation/helpers/Cheat.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Cheat.js +++ binaries/data/mods/public/simulation/helpers/Cheat.js @@ -109,22 +109,20 @@ if (!cmpTechnologyManager) return; - // store the phase we want in the next input parameter - let parameter; - if (!cmpTechnologyManager.IsTechnologyResearched("phase_town")) - parameter = "phase_town"; - else if (!cmpTechnologyManager.IsTechnologyResearched("phase_city")) - parameter = "phase_city"; - else - return; + const phases = [ "village", "town", "city" ]; - const civ = Engine.QueryInterface(playerEnt, IID_Identity).GetCiv(); - parameter += TechnologyTemplates.Has(parameter + "_" + civ) ? "_" + civ : "_generic"; + let technologyName = "technologies/{civ}/phase_"; + for (const phase of phases) + if (!cmpTechnologyManager.IsTechnologyResearched(technologyName + phase)) + { + technologyName += phase; + break; + } Cheat({ "player": input.player, "action": "researchTechnology", - "parameter": parameter, + "parameter": technologyName, "selected": input.selected }); return; @@ -172,8 +170,8 @@ } } } - - if (TechnologyTemplates.Has(techname)) + techname = techname.replace("{civ}", Engine.QueryInterface(playerEnt, IID_Identity).GetCiv()); + if (Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(techname)) cmpTechnologyManager.ResearchTechnology(techname); return; } Index: source/gui/GUIManager.h =================================================================== --- source/gui/GUIManager.h +++ source/gui/GUIManager.h @@ -125,6 +125,11 @@ const CParamNode& GetTemplate(const std::string& templateName); /** + * Retrieve a list of all available templates. + */ + const std::vector FindAllTemplates(bool actors); + + /** * Display progress / description in loading screen. */ void DisplayLoadProgress(int percent, const wchar_t* pending_task); Index: source/gui/GUIManager.cpp =================================================================== --- source/gui/GUIManager.cpp +++ source/gui/GUIManager.cpp @@ -24,6 +24,7 @@ #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" +#include "ps/TemplateLoader.h" #include "ps/Profile.h" #include "ps/VideoMode.h" #include "ps/XML/Xeromyces.h" @@ -423,6 +424,12 @@ return templateRoot; } +const std::vector CGUIManager::FindAllTemplates(bool includeActors) +{ + ETemplatesType templatesType = includeActors ? ALL_TEMPLATES : SIMULATION_TEMPLATES; + return m_TemplateLoader.FindTemplates("", true, templatesType); +} + void CGUIManager::DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(GetActiveGUI()->GetScriptInterface()); Index: source/gui/Scripting/JSInterface_GUIManager.cpp =================================================================== --- source/gui/Scripting/JSInterface_GUIManager.cpp +++ source/gui/Scripting/JSInterface_GUIManager.cpp @@ -73,6 +73,11 @@ return g_GUI->GetTemplate(templateName); } +std::vector FindAllTemplates(bool actors) +{ + return g_GUI->FindAllTemplates(actors); +} + void RegisterScriptFunctions(const ScriptRequest& rq) { @@ -83,6 +88,7 @@ ScriptFunction::Register<&ResetCursor>(rq, "ResetCursor"); ScriptFunction::Register<&TemplateExists>(rq, "TemplateExists"); ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate"); + ScriptFunction::Register<&FindAllTemplates>(rq, "FindAllTemplates"); ScriptFunction::Register<&CGUI::FindObjectByName, &ScriptInterface::ObjectFromCBData>(rq, "GetGUIObjectByName"); ScriptFunction::Register<&CGUI::SetGlobalHotkey, &ScriptInterface::ObjectFromCBData>(rq, "SetGlobalHotkey");