Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -437,7 +437,7 @@ }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; - ret.requiredTechnology = template.Identity.RequiredTechnology; + ret.requiredTechnology = template.Identity.Requirements?.Techs; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); ret.nativeCiv = template.Identity.Civ; } @@ -473,7 +473,7 @@ "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, - "requiredTechnology": upgrade.RequiredTechnology || undefined + "requiredTechnology": upgrade.Requirements?.Techs || undefined }); } } 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 @@ -293,8 +293,8 @@ if (parentTemplate.Upgrade[upgrade].Entity) return [inheritedVariance[0], TemplateVariant.upgrade, upgrade.toLowerCase()]; - if (template.Identity.RequiredTechnology) - return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.RequiredTechnology]; + if (template.Identity.Requirements?.Techs) + return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.Requirements?.Techs]; if (parentTemplate.Cost) for (let res in parentTemplate.Cost.Resources) Index: binaries/data/mods/public/simulation/components/Identity.js =================================================================== --- binaries/data/mods/public/simulation/components/Identity.js +++ binaries/data/mods/public/simulation/components/Identity.js @@ -76,9 +76,7 @@ "" + "" + "" + - "" + - "" + - "" + + RequirementsHelper.BuildSchema() + "" + "" + "" + 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 @@ -276,8 +276,8 @@ var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); - if (template.Identity && template.Identity.RequiredTechnology) - return this.IsTechnologyResearched(template.Identity.RequiredTechnology); + 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 return true; }; Index: binaries/data/mods/public/simulation/components/Upgrade.js =================================================================== --- binaries/data/mods/public/simulation/components/Upgrade.js +++ binaries/data/mods/public/simulation/components/Upgrade.js @@ -40,12 +40,7 @@ "" + "" + "" + - "" + - "" + - "" + - "" + - "" + - "" + + RequirementsHelper.BuildSchema() + "" + "" + "" + @@ -147,7 +142,7 @@ "icon": choice.Icon || undefined, "cost": hasCost ? cost : undefined, "tooltip": choice.Tooltip || undefined, - "requiredTechnology": this.GetRequiredTechnology(option), + "requirements": this.GetRequirements(option), }); } @@ -183,14 +178,14 @@ return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]]; }; -Upgrade.prototype.GetRequiredTechnology = function(templateArg) +Upgrade.prototype.GetRequirements = function(templateArg) { let choice = this.upgradeTemplates[templateArg] || templateArg; - if (this.template[choice].RequiredTechnology) - return this.template[choice].RequiredTechnology; + if (this.template[choice].Requirements) + return this.template[choice].Requirements; - if (!("RequiredTechnology" in this.template[choice])) + if (!("Requirements" in this.template[choice])) return undefined; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); @@ -201,7 +196,7 @@ entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv()); let template = cmpTemplateManager.GetTemplate(entType); - return template.Identity.RequiredTechnology || undefined; + return template.Identity.Requirements || undefined; }; Upgrade.prototype.GetResourceCosts = function(template) Index: binaries/data/mods/public/simulation/components/tests/test_Identity.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Identity.js +++ binaries/data/mods/public/simulation/components/tests/test_Identity.js @@ -1,3 +1,4 @@ +Engine.LoadHelperScript("Requirements.js"); Engine.LoadComponentScript("Identity.js"); let cmpIdentity = ConstructComponent(5, "Identity", { @@ -32,7 +33,9 @@ "Classes": { "_string": "CitizenSoldier Human Organic" }, "VisibleClasses": { "_string": "Javelineer" }, "Icon": "units/iber_infantry_javelineer.png", - "RequiredTechnology": "phase_town" + "Requirements": { + "Techs": "phase_town" + } }); TS_ASSERT_EQUALS(cmpIdentity.GetCiv(), "iber"); Index: binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js +++ binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js @@ -1,4 +1,5 @@ Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("Requirements.js"); Engine.LoadHelperScript("ValueModification.js"); Resources = { "BuildSchema": type => { Index: binaries/data/mods/public/simulation/helpers/Requirements.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/Requirements.js @@ -0,0 +1,179 @@ +function RequirementsHelper() {} + +RequirementsHelper.prototype.DEFAULT_RECURSION_DEPTH = 1; + +RequirementsHelper.prototype.EntityRequirementsSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + +RequirementsHelper.prototype.TechnologyRequirementsSchema = + "" + + "" + + "tokens" + + "" + + "" + + ""; + +/** + * @param {number} recursionDepth - How deep we recurse. + * @return {string} - A RelaxRNG schema for requirements. + */ +RequirementsHelper.prototype.RequirementsSchema = function(recursionDepth) +{ + return "" + + "" + + this.ChoicesSchema(--recursionDepth) + + ""; +}; + +/** + * @param {number} recursionDepth - How deep we recurse. + * @return {string} - A RelaxRNG schema for chosing requirements. + */ +RequirementsHelper.prototype.ChoicesSchema = function(recursionDepth) +{ + const allAnySchema = recursionDepth > 0 ? "" + + "" + + this.RequirementsSchema(recursionDepth) + + "" + + "" + + this.RequirementsSchema(recursionDepth) + + "" : ""; + + return "" + + "" + + allAnySchema + + this.EntityRequirementsSchema + + this.TechnologyRequirementsSchema + + ""; +}; + +/** + * @param {number} recursionDepth - How deeply recursive we build the schema. + * @return {string} - A RelaxRNG schema for requirements. + */ +RequirementsHelper.prototype.BuildSchema = function(recursionDepth = this.DEFAULT_RECURSION_DEPTH) +{ + return "" + + "" + + "" + + this.ChoicesSchema(recursionDepth) + + "" + + ""; +}; + +/** + * @param {Object} template - The requirements template as defined above. + * @param {number} playerID - The player ID to check the requirements for. + * @return {boolean} - Whether the requirements are met. + */ +RequirementsHelper.prototype.AreRequirementsMet = function(template, playerID) +{ + if (!template || !Object.keys(template).length) + return true; + + const cmpTechManager = QueryPlayerIDInterface(playerID, IID_TechnologyManager); + + if (template.All) + return this.AllRequirementsMet(template.All, cmpTechManager); + if (template.Any) + return this.AnyRequirementsMet(template.Any, cmpTechManager); + return true; +}; + +/** + * @param {Object} template - The requirements template for "all". + * @param {component} cmpTechManager - The technologyManager component to use when checking. + * @return {boolean} - Whether all given requirements are met. + */ +RequirementsHelper.prototype.AllRequirementsMet = function(template, cmpTechManager) +{ + for (const requirementType in template) + { + const requirement = template[requirementType]; + if (requirementType === "All" && !this.AllRequirementsMet(requirement, cmpTechManager)) + return false; + if (requirementType === "Any" && !this.AnyRequirementsMet(requirement, cmpTechManager)) + return false; + if (requirementType === "Entities") + { + for (const className in requirement) + { + const entReq = requirement[className]; + if ("Count" in entReq && (!(className in cmpTechManager.classCounts) || cmpTechManager.classCounts[className] < entReq.Count)) + return false; + if ("Variants" in entReq && (!(className in cmpTechManager.typeCountsByClass) || Object.keys(cmpTechManager.typeCountsByClass[className]).length < entReq.Variants)) + return false; + } + } + if (requirementType === "Techs") + { + for (const tech of requirement.split(" ")) + { + const negate = tech[0] === "!"; + if (negate && cmpTechManager.IsTechnologyResearched(tech.substring(1))) + return false; + if (!negate && !cmpTechManager.IsTechnologyResearched(tech)) + return false; + } + } + } + return true; +}; + +/** + * @param {Object} template - The requirements template for "any". + * @param {component} cmpTechManager - The technologyManager component to use when checking. + * @return {boolean} - Whether any of the given requirements is met. + */ +RequirementsHelper.prototype.AnyRequirementsMet = function(template, cmpTechManager) +{ + for (const requirementType in template) + { + const requirement = template[requirementType]; + if (requirementType === "All" && this.AllRequirementsMet(requirement, cmpTechManager)) + return true; + if (requirementType === "Any" && this.AnyRequirementsMet(requirement, cmpTechManager)) + return true; + if (requirementType === "Entities") + { + for (const className in requirement) + { + const entReq = requirement[className]; + if ("Count" in entReq && className in cmpTechManager.classCounts && cmpTechManager.classCounts[className] >= entReq.Count) + return true; + if ("Variants" in entReq && className in cmpTechManager.typeCountsByClass && Object.keys(cmpTechManager.typeCountsByClass[className]).length >= entReq.Variants) + return true; + } + } + if (requirementType === "Techs") + { + for (const tech of requirement.split(" ")) + { + const negate = tech[0] === "!"; + if (negate && !cmpTechManager.IsTechnologyResearched(tech.substring(1))) + return true; + if (!negate && cmpTechManager.IsTechnologyResearched(tech)) + return true; + } + } + } + return false; +}; + +Engine.RegisterGlobal("RequirementsHelper", new RequirementsHelper()); Index: binaries/data/mods/public/simulation/helpers/tests/test_Requirements.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/tests/test_Requirements.js @@ -0,0 +1,673 @@ +Engine.LoadComponentScript("interfaces/PlayerManager.js"); +Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("Requirements.js"); + +const playerID = 1; +const playerEnt = 11; + +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": () => playerEnt +}); + +// First test no requirements. +let template = { +}; + +const met = (bool) => { + if (bool) + TS_ASSERT(RequirementsHelper.AreRequirementsMet(template, playerID)); + else + TS_ASSERT(!RequirementsHelper.AreRequirementsMet(template, playerID)); +}; + +met(true); + + +// Additive requirements (all should to be met). +// Entity requirements. +template = { + "All": { + "Entities": { + "class_1": { + "Count": 1 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": {}, + "typeCountsByClass": {} +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 0 + }, + "typeCountsByClass": {} +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_2": 1 + }, + "typeCountsByClass": {} +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": {} +}); +met(true); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1 + } + } +}); +met(true); + + +template = { + "All": { + "Entities": { + "class_1": { + "Variants": 2 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(true); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1 + } + } +}); +met(false); + +template = { + "All": { + "Entities": { + "class_1": { + "Count": 1, + "Variants": 2 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(true); + +template = { + "All": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(true); + + +// Technology requirements. +template = { + "All": { + "Techs": "phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => false +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +met(true); + +template = { + "All": { + "Techs": "phase_city" + } +}; +met(false); + +template = { + "All": { + "Techs": "phase_town phase_city" + } +}; +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city" +}); +met(true); + +template = { + "All": { + "Techs": "!phase_city" + } +}; +met(false); + +template = { + "All": { + "Techs": "!phase_town phase_city" + } +}; +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +met(true); + + +// Combination of Entity and Technology requirements. +template = { + "All": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => false, + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(true); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(false); + + +// Choice requirements (at least one needs to be met). +// Entity requirements. +template = { + "Any": { + "Entities": { + "class_1": { + "Count": 1, + } + }, + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 0 + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + } +}); +met(true); + +template = { + "Any": { + "Entities": { + "class_1": { + "Count": 5, + "Variants": 2 + } + }, + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 3, + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(true); + + +// Technology requirements. +template = { + "Any": { + "Techs": "phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +met(true); + +template = { + "Any": { + "Techs": "phase_town phase_city" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +met(true); + +template = { + "Any": { + "Techs": "!phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +met(true); + +template = { + "Any": { + "Techs": "!phase_town phase_city" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city" +}); +met(true); + + +// Combinational requirements of entities and technologies. +template = { + "Any": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "!phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +met(true); + + +// Nested requirements. +template = { + "All": { + "All": { + "Techs": "!phase_town" + }, + "Any": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_city" + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(true); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => false, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(true); + + +template = { + "Any": { + "All": { + "Techs": "!phase_town" + }, + "Any": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_city" + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +met(true); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(true); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => false, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(true); + + +// Two levels deep nested. +template = { + "All": { + "Any": { + "All": { + "Techs": "cartography phase_imperial", + }, + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_city" + }, + "Techs": "!phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(true); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_imperial", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(false); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "cartography" || tech === "phase_imperial", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(true);