Index: ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 20749) +++ ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 20750) @@ -1,355 +1,386 @@ /** * This file contains shared logic for applying tech modifications in GUI, AI, * and simulation scripts. As such it must be fully deterministic and not store * any global state, but each context should do its own caching as needed. * Also it cannot directly access the simulation and requires data passed to it. */ /** * Returns modified property value modified by the applicable tech * modifications. * * @param currentTechModifications Object with mapping of property names to * modification arrays, retrieved from the intended player's TechnologyManager. * @param classes Array contianing the class list of the template. * @param propertyName String encoding the name of the value. * @param propertyValue Number storing the original value. Can also be * non-numberic, but then only "replace" techs can be supported. */ function GetTechModifiedProperty(currentTechModifications, classes, propertyName, propertyValue) { let modifications = currentTechModifications[propertyName] || []; let multiply = 1; let add = 0; for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (modification.replace !== undefined) return modification.replace; if (modification.multiply) multiply *= modification.multiply; else if (modification.add) add += modification.add; else warn("GetTechModifiedProperty: modification format not recognised (modifying " + propertyName + "): " + uneval(modification)); } // Note, some components pass non-numeric values (for which only the "replace" modification makes sense) if (typeof propertyValue == "number") return propertyValue * multiply + add; return propertyValue; } /** * Derives modifications (to be applied to entities) from a given technology. * * @param {Object} techTemplate - The technology template to derive the modifications from. * @return {Object} containing the relevant modifications. */ function DeriveModificationsFromTech(techTemplate) { if (!techTemplate.modifications) return {}; let techMods = {}; let techAffects = []; if (techTemplate.affects && techTemplate.affects.length) for (let affected of techTemplate.affects) techAffects.push(affected.split(/\s+/)); else techAffects.push([]); for (let mod of techTemplate.modifications) { let affects = techAffects.slice(); if (mod.affects) { let specAffects = mod.affects.split(/\s+/); for (let a in affects) affects[a] = affects[a].concat(specAffects); } let newModifier = { "affects": affects }; for (let idx in mod) if (idx !== "value" && idx !== "affects") newModifier[idx] = mod[idx]; if (!techMods[mod.value]) techMods[mod.value] = []; techMods[mod.value].push(newModifier); } return techMods; } /** * Derives modifications (to be applied to entities) from a provided array * of technology template data. * * @param {array} techsDataArray * @return {object} containing the combined relevant modifications of all * the technologies. */ function DeriveModificationsFromTechnologies(techsDataArray) { let derivedModifiers = {}; for (let technology of techsDataArray) { if (!technology.reqs) continue; let modifiers = DeriveModificationsFromTech(technology); for (let modPath in modifiers) { if (!derivedModifiers[modPath]) derivedModifiers[modPath] = []; derivedModifiers[modPath] = derivedModifiers[modPath].concat(modifiers[modPath]); } } return derivedModifiers; } /** * Returns whether the given modification applies to the entity containing the given class list */ function DoesModificationApply(modification, classes) { return MatchesClassList(classes, modification.affects); } /** * Derives the technology requirements from a given technology template. * Takes into account the `supersedes` attribute. * * @param {object} template - The template object. Loading of the template must have already occured. * * @return Derived technology requirements. See `InterpretTechRequirements` for object's syntax. */ function DeriveTechnologyRequirements(template, civ) { let requirements = []; if (template.requirements) { let op = Object.keys(template.requirements)[0]; let val = template.requirements[op]; requirements = InterpretTechRequirements(civ, op, val); } if (template.supersedes && requirements) { if (!requirements.length) requirements.push({}); for (let req of requirements) { if (!req.techs) req.techs = []; req.techs.push(template.supersedes); } } return requirements; } /** * Interprets the prerequisite requirements of a technology. * * Takes the initial { key: value } from the short-form requirements object in entity templates, * and parses it into an object that can be more easily checked by simulation and gui. * * Works recursively if needed. * * The returned object is in the form: * ``` * { "techs": ["tech1", "tech2"] }, * { "techs": ["tech3"] } * ``` * or * ``` * { "entities": [[{ * "class": "human", * "number": 2, * "check": "count" * } * or * ``` * false; * ``` * (Or, to translate: * 1. need either both `tech1` and `tech2`, or `tech3` * 2. need 2 entities with the `human` class * 3. cannot research this tech at all) * * @param {string} civ - The civ code * @param {string} operator - The base operation. Can be "civ", "notciv", "tech", "entity", "all" or "any". * @param {mixed} value - The value associated with the above operation. * * @return Object containing the requirements for the given civ, or false if the civ cannot research the tech. */ function InterpretTechRequirements(civ, operator, value) { let requirements = []; switch (operator) { case "civ": return !civ || civ == value ? [] : false; case "notciv": return civ == value ? false : []; case "entity": { let number = value.number || value.numberOfTypes || 0; if (number > 0) requirements.push({ "entities": [{ "class": value.class, "number": number, "check": value.number ? "count" : "variants" }] }); break; } case "tech": requirements.push({ "techs": [value] }); break; case "all": { let civPermitted = undefined; // tri-state (undefined, false, or true) for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) civPermitted = true; else if (civPermitted !== true) civPermitted = false; break; case "notciv": if (!result) return false; break; case "any": if (!result) return false; // else, fall through case "all": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) civPermitted = false; continue; } // else, fall through case "tech": case "entity": { if (result.length) { if (!requirements.length) requirements.push({}); let newRequirements = []; for (let currReq of requirements) for (let res of result) { let newReq = {}; for (let subtype in currReq) newReq[subtype] = currReq[subtype]; for (let subtype in res) { if (!newReq[subtype]) newReq[subtype] = []; newReq[subtype] = newReq[subtype].concat(res[subtype]); } newRequirements.push(newReq); } requirements = newRequirements; } break; } } } if (civPermitted === false) // if and only if false return false; break; } case "any": { let civPermitted = false; for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) return []; break; case "notciv": if (!result) return false; civPermitted = true; break; case "any": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) continue; return false; } // else, fall through case "all": if (!result) continue; civPermitted = true; // else, fall through case "tech": case "entity": for (let res of result) requirements.push(res); break; } } if (!civPermitted && !requirements.length) return false; break; } default: warn("Unknown requirement operator: "+operator); } return requirements; } + +/** + * Determine order of phases. + * + * @param {object} phases - The current available store of phases. + * @return {array} List of phases + */ +function UnravelPhases(phases) +{ + let phaseMap = {}; + for (let phaseName in phases) + { + let phaseData = phases[phaseName]; + if (!phaseData.reqs.length || !phaseData.reqs[0].techs || !phaseData.replaces) + continue; + + 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; + } + + let phaseList = Object.keys(phaseMap); + phaseList.sort((a, b) => phaseList.indexOf(a) - phaseList.indexOf(phaseMap[b])); + + return phaseList; +} Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 20749) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 20750) @@ -1,529 +1,530 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ "Code", "Culture", "Name", "Emblem", "History", "Music", "Factions", "CivBonuses", "TeamBonuses", "Structures", "StartEntities", "Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); if (!selectableOnly || data.SelectableInGameSetup) civData[data.Code] = data; } return civData; } /** * Gets an array of all classes for this identity template */ function GetIdentityClasses(template) { var classList = []; if (template.Classes && template.Classes._string) classList = classList.concat(template.Classes._string.split(/\s+/)); if (template.VisibleClasses && template.VisibleClasses._string) classList = classList.concat(template.VisibleClasses._string.split(/\s+/)); if (template.Rank) classList = classList.concat(template.Rank); return classList; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { if (template.VisibleClasses && template.VisibleClasses._string) return template.VisibleClasses._string.split(/\s+/); return []; } /** * Check if a given list of classes matches another list of classes. * Useful f.e. for checking identity classes. * * @param classes - List of the classes to check against. * @param match - Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2. * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed. * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed. * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise. */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // Transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (let sublist of match) { // If the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) || (c[0] != "!" && classes.indexOf(c) != -1))) return true; } return false; } /** * Gets the value originating at the value_path as-is, with no modifiers applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers) current_value = GetTechModifiedProperty(modifiers, GetIdentityClasses(template.Identity), mod_key, current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {object} resources - An instance of the Resources prototype. * @param {object} damageTypes - An instance of the DamageTypes prototype. * @param {object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, resources, damageTypes, modifiers={}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.Armour) { ret.armour = {}; for (let damageType of damageTypes.GetTypes()) ret.armour[damageType] = getEntityValue("Armour/" + damageType); } if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; if (type == "Capture") ret.attack.Capture = { "value": getAttackStat("Value") }; else { ret.attack[type] = { "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus") }; for (let damageType of damageTypes.GetTypes()) ret.attack[type][damageType] = getAttackStat(damageType); ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); } ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Splash) { ret.attack[type].splash = { // true if undefined "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape }; for (let damageType of damageTypes.GetTypes()) ret.attack[type].splash[damageType] = getAttackStat("Splash/" + damageType); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false" }; for (let damageType of damageTypes.GetTypes()) ret.deathDamage[damageType] = getEntityValue("DeathDamage/" + damageType); } if (template.Auras && auraTemplates) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) { let aura = auraTemplates[auraID]; ret.auras[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": aura.radius || null }; } } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.PopulationBonus) ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "hp": getEntityValue("Heal/HP"), "range": getEntityValue("Heal/Range"), "rate": getEntityValue("Heal/Rate") }; if (template.ResourceGatherer) { ret.resourceGatherRates = {}; let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed"); for (let type in template.ResourceGatherer.Rates) ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed; } if (template.ResourceTrickle) { ret.resourceTrickle = { "interval": +template.ResourceTrickle.Interval, "rates": {} }; for (let type in template.ResourceTrickle.Rates) ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type); } if (template.Loot) { ret.loot = {}; for (let type in template.Loot) ret.loot[type] = getEntityValue("Loot/"+ type); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else ret.obstruction.shape.type = "cluster"; } if (template.Pack) ret.pack = { "state": template.Pack.State, "time": getEntityValue("Pack/Time"), }; if (template.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; if (template.UnitMotion.Run) ret.speed.run = getEntityValue("UnitMotion/Run/Speed"); } if (template.Upgrade) { ret.upgrades = []; for (let upgradeName in template.Upgrade) { let upgrade = template.Upgrade[upgradeName]; let cost = {}; if (upgrade.Cost) for (let res in upgrade.Cost) cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); if (upgrade.Time) cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); ret.upgrades.push({ "entity": upgrade.Entity, "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, "requiredTechnology": upgrade.RequiredTechnology || undefined }); } } if (template.ProductionQueue) { ret.techCostMultiplier = {}; for (let res in template.ProductionQueue.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "_fortress", "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap }; if (template.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(" "); } if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length, "angle": +(template.WallPiece.Orientation || 1) * Math.PI, "indent": +(template.WallPiece.Indent || 0), "bend": +(template.WallPiece.Bend || 0) * Math.PI }; return ret; } /** * 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 + "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. */ 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; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/helper.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/helper.js (revision 20749) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/helper.js (revision 20750) @@ -1,197 +1,152 @@ var g_CurrentModifiers = {}; function deriveModifications(techList) { let techData = []; for (let techName of techList) techData.push(GetTechnologyBasicDataHelper(loadTechData(techName), g_SelectedCiv)); return DeriveModificationsFromTechnologies(techData); } /** * Provided with an array containing basic information about possible * upgrades, such as that generated by globalscript's GetTemplateDataHelper, * this function loads the actual template data of the upgrades, overwrites * certain values within, then passes an array containing the template data * back to caller. */ function getActualUpgradeData(upgradesInfo) { let newUpgrades = []; for (let upgrade of upgradesInfo) { upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, g_SelectedCiv); let data = GetTemplateDataHelper(loadTemplate(upgrade.entity), null, g_AuraData, g_ResourceData, g_DamageTypes); data.cost = upgrade.cost; data.icon = upgrade.icon || data.icon; data.tooltip = upgrade.tooltip || data.tooltip; data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology; newUpgrades.push(data); } return newUpgrades; } /** * Determines and returns the phase in which a given technology can be * first researched. Works recursively through the given tech's * pre-requisite and superseded techs if necessary. * * @param {string} techName - The Technology's name * @return The name of the phase the technology belongs to, or false if * the current civ can't research this tech */ function getPhaseOfTechnology(techName) { let phaseIdx = -1; if (basename(techName).startsWith("phase")) { if (!g_ParsedData.phases[techName].reqs) return false; phaseIdx = g_ParsedData.phaseList.indexOf(getActualPhase(techName)); if (phaseIdx > 0) return g_ParsedData.phaseList[phaseIdx - 1]; } if (!g_ParsedData.techs[g_SelectedCiv][techName]) { let techData = loadTechnology(techName); g_ParsedData.techs[g_SelectedCiv][techName] = techData; warn("The \"" + techData.name.generic + "\" technology is not researchable in any structure buildable by the " + g_SelectedCiv + " civilisation, but is required by something that this civ can research, train or build!"); } let techReqs = g_ParsedData.techs[g_SelectedCiv][techName].reqs; 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, g_ParsedData.phaseList.indexOf(getPhaseOfTechnology(tech))); } return g_ParsedData.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} */ function getActualPhase(phaseName) { if (g_ParsedData.phases[phaseName]) return g_ParsedData.phases[phaseName].actualPhase; warn("Unrecognised phase (" + phaseName + ")"); return g_ParsedData.phaseList[0]; } /** * Returns the required phase of a given unit or structure. * * @param {object} template * @return {string} */ function getPhaseOfTemplate(template) { if (!template.requiredTechnology) return g_ParsedData.phaseList[0]; if (basename(template.requiredTechnology).startsWith("phase")) return getActualPhase(template.requiredTechnology); return getPhaseOfTechnology(template.requiredTechnology); } /** - * Determine order of phases. - * - * @param {object} phases - The current available store of phases. - * @return {array} List of phases - */ -function unravelPhases(phases) -{ - let phaseList = []; - - for (let phaseName in phases) - { - let phaseData = phases[phaseName]; - if (!phaseData.reqs.length || !phaseData.reqs[0].techs) - continue; - - let myPhase = phaseData.actualPhase; - let reqPhase = phaseData.reqs[0].techs[0]; - if (phases[reqPhase]) - reqPhase = phases[reqPhase].actualPhase; - - let reqPhasePos = phaseList.indexOf(reqPhase); - let myPhasePos = phaseList.indexOf(myPhase); - - // Sort the phases in the order they can be researched - if (!phaseList.length) - phaseList = [reqPhase, myPhase]; - else if (reqPhasePos < 0 && myPhasePos != -1) - phaseList.splice(myPhasePos, 0, reqPhase); - else if (myPhasePos < 0 && reqPhasePos != -1) - phaseList.splice(reqPhasePos+1, 0, myPhase); - else if (reqPhasePos > myPhasePos) - { - phaseList.splice(reqPhasePos+1, 0, myPhase); - phaseList.splice(myPhasePos, 1); - } - else if (reqPhasePos < 0 && myPhasePos < 0) - // If neither phase is in the list, then add them both to the end and - // rely on later iterations relocating them to their correct place. - phaseList.push(reqPhase, myPhase); - } - - return phaseList; -} - -/** * This is needed because getEntityCostTooltip in tooltip.js needs to get * the template data of the different wallSet pieces. In the session this * function does some caching, but here we do that in loadTemplate already. */ function GetTemplateData(templateName) { var template = loadTemplate(templateName); return GetTemplateDataHelper(template, null, g_AuraData, g_ResourceData, g_DamageTypes, g_CurrentModifiers); } function isPairTech(technologyCode) { return !!loadTechData(technologyCode).top; } function mergeRequirements(reqsA, reqsB) { if (reqsA === false || reqsB === false) return false; let finalReqs = clone(reqsA); for (let option of reqsB) for (let type in option) for (let opt in finalReqs) { if (!finalReqs[opt][type]) finalReqs[opt][type] = []; finalReqs[opt][type] = finalReqs[opt][type].concat(option[type]); } return finalReqs; } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/load.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/load.js (revision 20749) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/load.js (revision 20750) @@ -1,358 +1,357 @@ /** * Paths to certain files. */ const g_TechnologyPath = "simulation/data/technologies/"; const g_AuraPath = "simulation/data/auras/"; /** * Raw Data Caches. */ var g_AuraData = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_CivData = loadCivData(true, false); /** * Parsed Data Stores. */ var g_ParsedData = {}; var g_ResourceData = new Resources(); var g_DamageTypes = new DamageTypes(); // This must be defined after the g_TechnologyData cache object is declared. var g_AutoResearchTechList = findAllAutoResearchedTechs(); /** * Loads raw entity template. * * Loads from local cache if data present, else from file system. * * @param {string} templateName * @return {object} Object containing raw template data. */ function loadTemplate(templateName) { if (!(templateName in g_TemplateData)) { // We need to clone the template because we want to perform some translations. let data = clone(Engine.GetTemplate(templateName)); translateObjectKeys(data, ["GenericName", "SpecificName", "Tooltip"]); if (data.Auras) for (let auraID of data.Auras._string.split(/\s+/)) loadAuraData(auraID); g_TemplateData[templateName] = data; } return g_TemplateData[templateName]; } /** * Loads raw technology template. * * Loads from local cache if available, else from file system. * * @param {string} templateName * @return {object} Object containing raw template data. */ function loadTechData(templateName) { if (!(templateName in g_TechnologyData)) { let data = Engine.ReadJSONFile(g_TechnologyPath + templateName + ".json"); translateObjectKeys(data, ["genericName", "tooltip"]); g_TechnologyData[templateName] = data; } return g_TechnologyData[templateName]; } function techDataExists(templateName) { return Engine.FileExists("simulation/data/technologies/" + templateName + ".json"); } /** * Loads raw aura template. * * Loads from local cache if available, else from file system. * * @param {string} templateName * @return {object} Object containing raw template data. */ function loadAuraData(templateName) { if (!(templateName in g_AuraData)) { let data = Engine.ReadJSONFile(g_AuraPath + templateName + ".json"); translateObjectKeys(data, ["auraName", "auraDescription"]); g_AuraData[templateName] = data; } return g_AuraData[templateName]; } /** * Load and parse unit from entity template. * * @param {string} templateName * @return Sanitized object about the requested unit or null if entity template doesn't exist. */ function loadUnit(templateName) { if (!Engine.TemplateExists(templateName)) return null; let template = loadTemplate(templateName); let unit = GetTemplateDataHelper(template, null, g_AuraData, g_ResourceData, g_DamageTypes, g_CurrentModifiers); if (template.ProductionQueue) { unit.production = {}; if (template.ProductionQueue.Entities) { unit.production.units = []; for (let build of template.ProductionQueue.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv); if (Engine.TemplateExists(build)) unit.production.units.push(build); } } if (template.ProductionQueue.Technologies) { unit.production.techs = []; for (let research of template.ProductionQueue.Technologies._string.split(" ")) { if (research.indexOf("{civ}") != -1) { let civResearch = research.replace("{civ}", g_SelectedCiv); research = techDataExists(civResearch) ? civResearch : research.replace("{civ}", "generic"); } if (isPairTech(research)) for (let tech of loadTechnologyPair(research).techs) unit.production.techs.push(tech); else unit.production.techs.push(research); } } } if (template.Builder && template.Builder.Entities._string) { unit.builder = []; for (let build of template.Builder.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv); if (Engine.TemplateExists(build)) unit.builder.push(build); } } if (unit.upgrades) unit.upgrades = getActualUpgradeData(unit.upgrades); return unit; } /** * Load and parse structure from entity template. * * @param {string} templateName * @return {object} Sanitized data about the requested structure or null if entity template doesn't exist. */ function loadStructure(templateName) { if (!Engine.TemplateExists(templateName)) return null; let template = loadTemplate(templateName); let structure = GetTemplateDataHelper(template, null, g_AuraData, g_ResourceData, g_DamageTypes, g_CurrentModifiers); structure.production = { "technology": [], "units": [] }; if (template.ProductionQueue) { if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string) for (let build of template.ProductionQueue.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv); if (Engine.TemplateExists(build)) structure.production.units.push(build); } if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string) for (let research of template.ProductionQueue.Technologies._string.split(" ")) { if (research.indexOf("{civ}") != -1) { let civResearch = research.replace("{civ}", g_SelectedCiv); research = techDataExists(civResearch) ? civResearch : research.replace("{civ}", "generic"); } if (isPairTech(research)) for (let tech of loadTechnologyPair(research).techs) structure.production.technology.push(tech); else structure.production.technology.push(research); } } if (structure.upgrades) structure.upgrades = getActualUpgradeData(structure.upgrades); if (structure.wallSet) { structure.wallset = {}; if (!structure.upgrades) structure.upgrades = []; // Note: An assumption is made here that wall segments all have the same armor and auras let struct = loadStructure(structure.wallSet.templates.long); structure.armour = struct.armour; structure.auras = struct.auras; // For technology cost multiplier, we need to use the tower struct = loadStructure(structure.wallSet.templates.tower); structure.techCostMultiplier = struct.techCostMultiplier; let health; for (let wSegm in structure.wallSet.templates) { if (wSegm == "fort" || wSegm == "curves") continue; let wPart = loadStructure(structure.wallSet.templates[wSegm]); structure.wallset[wSegm] = wPart; for (let research of wPart.production.technology) structure.production.technology.push(research); if (wPart.upgrades) structure.upgrades = structure.upgrades.concat(wPart.upgrades); if (["gate", "tower"].indexOf(wSegm) != -1) continue; if (!health) { health = { "min": wPart.health, "max": wPart.health }; continue; } health.min = Math.min(health.min, wPart.health); health.max = Math.max(health.max, wPart.health); } if (structure.wallSet.templates.curves) for (let curve of structure.wallSet.templates.curves) { let wPart = loadStructure(curve); health.min = Math.min(health.min, wPart.health); health.max = Math.max(health.max, wPart.health); } if (health.min == health.max) structure.health = health.min; else structure.health = sprintf(translate("%(health_min)s to %(health_max)s"), { "health_min": health.min, "health_max": health.max }); } return structure; } /** * Load and parse technology from json template. * * @param {string} templateName * @return {object} Sanitized data about the requested technology. */ function loadTechnology(techName) { let template = loadTechData(techName); let tech = GetTechnologyDataHelper(template, g_SelectedCiv, g_ResourceData); if (template.pair !== undefined) { tech.pair = template.pair; tech.reqs = mergeRequirements(tech.reqs, loadTechnologyPair(template.pair).reqs); } return tech; } /** * Crudely iterates through every tech JSON file and identifies those * that are auto-researched. * * @return {array} List of techs that are researched automatically */ function findAllAutoResearchedTechs() { let techList = []; for (let filename of Engine.ListDirectoryFiles(g_TechnologyPath, "*.json", true)) { // -5 to strip off the file extension let templateName = filename.slice(g_TechnologyPath.length, -5); let data = loadTechData(templateName); if (data && data.autoResearch) techList.push(templateName); } return techList; } /** * @param {string} phaseCode * @return {object} Sanitized object containing phase data */ function loadPhase(phaseCode) { - let template = loadTechData(phaseCode); let phase = loadTechnology(phaseCode); phase.actualPhase = phaseCode; - if (template.replaces !== undefined) - phase.actualPhase = template.replaces[0]; + if (phase.replaces !== undefined) + phase.actualPhase = phase.replaces[0]; return phase; } /** * @param {string} pairCode * @return {object} Contains a list and the requirements of the techs in the pair */ function loadTechnologyPair(pairCode) { var pairInfo = loadTechData(pairCode); return { "techs": [ pairInfo.top, pairInfo.bottom ], "reqs": DeriveTechnologyRequirements(pairInfo, g_SelectedCiv) }; } /** * @param {string} modCode * @return {object} Sanitized object containing modifier tech data */ function loadModifierTech(modCode) { if (!Engine.FileExists("simulation/data/technologies/"+modCode+".json")) return {}; return loadTechData(modCode); } Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 20749) +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 20750) @@ -1,184 +1,184 @@ /** * Array of structure template names when given a civ and a phase name. */ var g_BuildList = {}; /** * Array of template names that can be trained from a unit, given a civ and unit template name. */ var g_TrainList = {}; /** * Initialize the page * * @param {object} data - Parameters passed from the code that calls this page into existence. */ function init(data = {}) { if (data.callback) g_CallbackSet = true; let civList = Object.keys(g_CivData).map(civ => ({ "name": g_CivData[civ].Name, "code": civ, })).sort(sortNameIgnoreCase); if (!civList.length) { closePage(); return; } g_ParsedData = { "units": {}, "structures": {}, "techs": {}, "phases": {} }; let civSelection = Engine.GetGUIObjectByName("civSelection"); civSelection.list = civList.map(c => c.name); civSelection.list_data = civList.map(c => c.code); civSelection.selected = data.civ ? civSelection.list_data.indexOf(data.civ) : 0; } /** * @param {string} civCode */ function selectCiv(civCode) { if (civCode === g_SelectedCiv || !g_CivData[civCode]) return; g_SelectedCiv = civCode; g_CurrentModifiers = deriveModifications(g_AutoResearchTechList); // If a buildList already exists, then this civ has already been parsed if (g_BuildList[g_SelectedCiv]) { draw(); drawPhaseIcons(); return; } let templateLists = compileTemplateLists(civCode); for (let u of templateLists.units.keys()) if (!g_ParsedData.units[u]) g_ParsedData.units[u] = loadUnit(u); for (let s of templateLists.structures.keys()) if (!g_ParsedData.structures[s]) g_ParsedData.structures[s] = loadStructure(s); // Load technologies g_ParsedData.techs[civCode] = {}; for (let techcode of templateLists.techs.keys()) if (basename(techcode).startsWith("phase")) g_ParsedData.phases[techcode] = loadPhase(techcode); else g_ParsedData.techs[civCode][techcode] = loadTechnology(techcode); // Establish phase order - g_ParsedData.phaseList = unravelPhases(g_ParsedData.phases); + g_ParsedData.phaseList = UnravelPhases(g_ParsedData.phases); // Load any required generic phases that aren't already loaded for (let phasecode of g_ParsedData.phaseList) if (!g_ParsedData.phases[phasecode]) g_ParsedData.phases[phasecode] = loadPhase(phasecode); // Group production and upgrade lists of structures by phase for (let structCode of templateLists.structures.keys()) { let structInfo = g_ParsedData.structures[structCode]; structInfo.phase = getPhaseOfTemplate(structInfo); let structPhaseIdx = g_ParsedData.phaseList.indexOf(structInfo.phase); // If this building is shared with another civ, // it may have already gone through the grouping process already if (!Array.isArray(structInfo.production.technology)) continue; // Sort techs by phase let newProdTech = {}; for (let prod of structInfo.production.technology) { let phase = getPhaseOfTechnology(prod); if (phase === false) continue; if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!(phase in newProdTech)) newProdTech[phase] = []; newProdTech[phase].push(prod); } // Sort units by phase let newProdUnits = {}; for (let prod of structInfo.production.units) { let phase = getPhaseOfTemplate(g_ParsedData.units[prod]); if (phase === false) continue; if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!(phase in newProdUnits)) newProdUnits[phase] = []; newProdUnits[phase].push(prod); } g_ParsedData.structures[structCode].production = { "technology": newProdTech, "units": newProdUnits }; // Sort upgrades by phase let newUpgrades = {}; if (structInfo.upgrades) for (let upgrade of structInfo.upgrades) { let phase = getPhaseOfTemplate(upgrade); if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!newUpgrades[phase]) newUpgrades[phase] = []; newUpgrades[phase].push(upgrade); } g_ParsedData.structures[structCode].upgrades = newUpgrades; } // Determine the buildList for the civ (grouped by phase) let buildList = {}; let trainerList = []; for (let pha of g_ParsedData.phaseList) buildList[pha] = []; for (let structCode of templateLists.structures.keys()) { let phase = g_ParsedData.structures[structCode].phase; buildList[phase].push(structCode); } for (let unitCode of templateLists.units.keys()) { let unitTemplate = g_ParsedData.units[unitCode]; if ((!unitTemplate.production || !Object.keys(unitTemplate.production).length) && !unitTemplate.upgrades) continue; trainerList.push(unitCode); } g_BuildList[g_SelectedCiv] = buildList; g_TrainList[g_SelectedCiv] = trainerList; draw(); drawPhaseIcons(); } Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20749) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20750) @@ -1,943 +1,933 @@ var API3 = function(m) { /** * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ m.GameState = function() { this.ai = null; // must be updated by the AIs. }; m.GameState.prototype.init = function(SharedScript, state, player) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.timeElapsed = SharedScript.timeElapsed; this.circularMap = SharedScript.circularMap; this.templates = SharedScript._templates; this.entities = SharedScript.entities; this.player = player; this.playerData = SharedScript.playersData[this.player]; this.gameType = SharedScript.gameType; this.alliedVictory = SharedScript.alliedVictory; this.ceasefireActive = SharedScript.ceasefireActive; this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; // get the list of possible phases for this civ: // we assume all of them are researchable from the civil centre - this.phases = [{ name: "phase_village" }, { name: "phase_town" }, { name: "phase_city" }]; + this.phases = []; let cctemplate = this.getTemplate(this.applyCiv("structures/{civ}_civil_centre")); if (!cctemplate) return; let civ = this.getPlayerCiv(); let techs = cctemplate.researchableTechs(this, civ); - for (let phase of this.phases) + + let phaseData = {}; + let phaseMap = {}; + for (let techName of techs) { - phase.requirements = []; - let k = techs.indexOf(phase.name); - if (k !== -1) - { - let reqs = DeriveTechnologyRequirements(this.getTemplate(techs[k])._template, civ); - if (reqs) - { - phase.requirements = reqs; - continue; - } - } - for (let tech of techs) + if (!techName.startsWith("phase")) + continue; + let techData = this.getTemplate(techName); + + if (techData._definesPair) { - let template = this.getTemplate(tech)._template; - if (template.replaces && template.replaces.indexOf(phase.name) != -1) - { - let reqs = DeriveTechnologyRequirements(template, civ); - if (reqs) - { - phase.name = tech; - phase.requirements = reqs; - break; - } - } + // Randomly pick a non-disabled choice from the phase-pair. + techName = pickRandom([techData._template.top, techData._template.bottom].filter(tech => !this.playerData.disabledTechnologies[tech])) || techData._template.top; + + let supersedes = techData._template.supersedes; + techData = clone(this.getTemplate(techName)); + if (supersedes) + techData._template.supersedes = supersedes; } + + phaseData[techName] = GetTechnologyBasicDataHelper(techData._template, civ); + if (phaseData[techName].replaces) + phaseMap[phaseData[techName].replaces[0]] = techName; } - // Then check if this mod has an additionnal phase - for (let tech of techs) - { - let template = this.getTemplate(tech)._template; - if (!template.supersedes || template.supersedes != this.phases[2].name) - continue; - let reqs = DeriveTechnologyRequirements(template, civ); - if (reqs) - this.phases.push({ "name": tech, "requirements": reqs }); - break; - } + + this.phases = UnravelPhases(phaseData).map(phaseName => ({ + "name": phaseMap[phaseName] || phaseName, + "requirements": phaseMap[phaseName] ? phaseData[phaseMap[phaseName]].reqs : [] + })); }; m.GameState.prototype.update = function(SharedScript) { this.timeElapsed = SharedScript.timeElapsed; this.playerData = SharedScript.playersData[this.player]; this.ceasefireActive = SharedScript.ceasefireActive; this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; }; m.GameState.prototype.updatingCollection = function(id, filter, parentCollection) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID return this.updatingGlobalCollection(gid, filter, parentCollection); }; m.GameState.prototype.destroyCollection = function(id) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID this.destroyGlobalCollection(gid); }; m.GameState.prototype.updatingGlobalCollection = function(gid, filter, parentCollection) { if (this.EntCollecNames.has(gid)) return this.EntCollecNames.get(gid); let collection = parentCollection ? parentCollection.filter(filter) : this.entities.filter(filter); collection.registerUpdates(); this.EntCollecNames.set(gid, collection); return collection; }; m.GameState.prototype.destroyGlobalCollection = function(gid) { if (!this.EntCollecNames.has(gid)) return; this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(gid)); this.EntCollecNames.delete(gid); }; /** * Reset the entities collections which depend on diplomacy */ m.GameState.prototype.resetOnDiplomacyChanged = function() { for (let name of this.EntCollecNames.keys()) if (name.startsWith("player-" + this.player + "-diplo")) this.destroyGlobalCollection(name); }; m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; m.GameState.prototype.getBarterPrices = function() { return this.playerData.barterPrices; }; m.GameState.prototype.getGameType = function() { return this.gameType; }; m.GameState.prototype.getAlliedVictory = function() { return this.alliedVictory; }; m.GameState.prototype.isCeasefireActive = function() { return this.ceasefireActive; }; m.GameState.prototype.getTemplate = function(type) { if (TechnologyTemplates.Has(type)) return new m.Technology(type); if (this.templates[type] === undefined) this.sharedScript.GetTemplate(type); return this.templates[type] ? new m.Template(this.sharedScript, type, this.templates[type]) : null; }; /** Return the template of the structure built from this foundation */ m.GameState.prototype.getBuiltTemplate = function(foundationName) { if (!foundationName.startsWith("foundation|")) { warn("Foundation " + foundationName + " not recognised as a foundation."); return null; } return this.getTemplate(foundationName.substr(11)); }; m.GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; m.GameState.prototype.getPlayerCiv = function(player) { return player !== undefined ? this.sharedScript.playersData[player].civ : this.playerData.civ; }; m.GameState.prototype.currentPhase = function() { for (let i = this.phases.length; i > 0; --i) if (this.isResearched(this.phases[i-1].name)) return i; return 0; }; m.GameState.prototype.getNumberOfPhases = function() { return this.phases.length; }; m.GameState.prototype.getPhaseName = function(i) { return this.phases[i-1] ? this.phases[i-1].name : undefined; }; m.GameState.prototype.getPhaseEntityRequirements = function(i) { let entityReqs = []; for (let requirement of this.phases[i-1].requirements) { if (!requirement.entities) continue; for (let entity of requirement.entities) if (entity.check == "count") entityReqs.push({ "class": entity.class, "count": entity.number }); } return entityReqs; }; m.GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs.has(template); }; /** true if started or queued */ m.GameState.prototype.isResearching = function(template) { return this.playerData.researchStarted.has(template) || this.playerData.researchQueued.has(template); }; /** this is an "in-absolute" check that doesn't check if we have a building to research from. */ m.GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) { if (this.playerData.disabledTechnologies[techTemplateName]) return false; let template = this.getTemplate(techTemplateName); if (!template) return false; // researching or already researched: NOO. if (this.playerData.researchQueued.has(techTemplateName) || this.playerData.researchStarted.has(techTemplateName) || this.playerData.researchedTechs.has(techTemplateName)) return false; if (noRequirementCheck) return true; // if this is a pair, we must check that the pair tech is not being researched if (template.pair()) { let other = template.pairedWith(); if (this.playerData.researchQueued.has(other) || this.playerData.researchStarted.has(other) || this.playerData.researchedTechs.has(other)) return false; } return this.checkTechRequirements(template.requirements(this.playerData.civ)); }; /** * Private function for checking a set of requirements is met. * Basically copies TechnologyManager, but compares against * variables only available within the AI */ m.GameState.prototype.checkTechRequirements = function(reqs) { if (!reqs) return false; if (!reqs.length) return true; function doesEntitySpecPass(entity) { switch (entity.check) { case "count": if (!this.playerData.classCounts[entity.class] || this.playerData.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.playerData.typeCountsByClass[entity.class] || Object.keys(this.playerData.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; } return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(tech => this.playerData.researchedTechs.has(tech)); case "entities": return req[type].every(doesEntitySpecPass, this); } return false; }); }); }; m.GameState.prototype.getPassabilityMap = function() { return this.sharedScript.passabilityMap; }; m.GameState.prototype.getPassabilityClassMask = function(name) { if (!this.sharedScript.passabilityClasses[name]) error("Tried to use invalid passability class name '" + name + "'"); return this.sharedScript.passabilityClasses[name]; }; m.GameState.prototype.getResources = function() { return new m.Resources(this.playerData.resourceCounts); }; m.GameState.prototype.getPopulation = function() { return this.playerData.popCount; }; m.GameState.prototype.getPopulationLimit = function() { return this.playerData.popLimit; }; m.GameState.prototype.getPopulationMax = function() { return this.playerData.popMax; }; m.GameState.prototype.getPlayerID = function() { return this.player; }; m.GameState.prototype.hasAllies = function() { for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasEnemies = function() { for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i] && +i !== 0 && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasNeutrals = function() { for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i] && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.isPlayerNeutral = function(id) { return this.playerData.isNeutral[id]; }; m.GameState.prototype.isPlayerAlly = function(id) { return this.playerData.isAlly[id]; }; m.GameState.prototype.isPlayerMutualAlly = function(id) { return this.playerData.isMutualAlly[id]; }; m.GameState.prototype.isPlayerEnemy = function(id) { return this.playerData.isEnemy[id]; }; /** Return the number of players currently enemies, not including gaia */ m.GameState.prototype.getNumPlayerEnemies = function() { let num = 0; for (let i = 1; i < this.playerData.isEnemy.length; ++i) if (this.playerData.isEnemy[i] && this.sharedScript.playersData[i].state != "defeated") ++num; return num; }; m.GameState.prototype.getEnemies = function() { let ret = []; for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i]) ret.push(+i); return ret; }; m.GameState.prototype.getNeutrals = function() { let ret = []; for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i]) ret.push(+i); return ret; }; m.GameState.prototype.getAllies = function() { let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i]) ret.push(+i); return ret; }; m.GameState.prototype.getExclusiveAllies = function() { // Player is not included let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player) ret.push(+i); return ret; }; m.GameState.prototype.getMutualAllies = function() { let ret = []; for (let i in this.playerData.isMutualAlly) if (this.playerData.isMutualAlly[i] && this.sharedScript.playersData[i].isMutualAlly[this.player]) ret.push(+i); return ret; }; m.GameState.prototype.isEntityAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()]; }; m.GameState.prototype.isEntityExclusiveAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; }; m.GameState.prototype.isEntityEnemy = function(ent) { if (!ent) return false; return this.playerData.isEnemy[ent.owner()]; }; m.GameState.prototype.isEntityOwn = function(ent) { if (!ent) return false; return ent.owner() === this.player; }; m.GameState.prototype.getEntityById = function(id) { if (this.entities._entities.has(+id)) return this.entities._entities.get(+id); return undefined; }; m.GameState.prototype.getEntities = function(id) { if (id === undefined) return this.entities; return this.updatingGlobalCollection("player-" + id + "-entities", m.Filters.byOwner(id)); }; m.GameState.prototype.getStructures = function() { return this.updatingGlobalCollection("structures", m.Filters.byClass("Structure"), this.entities); }; m.GameState.prototype.getOwnEntities = function() { return this.updatingGlobalCollection("player-" + this.player + "-entities", m.Filters.byOwner(this.player)); }; m.GameState.prototype.getOwnStructures = function() { return this.updatingGlobalCollection("player-" + this.player + "-structures", m.Filters.byClass("Structure"), this.getOwnEntities()); }; m.GameState.prototype.getOwnUnits = function() { return this.updatingGlobalCollection("player-" + this.player + "-units", m.Filters.byClass("Unit"), this.getOwnEntities()); }; m.GameState.prototype.getAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getAllies())); }; m.GameState.prototype.getExclusiveAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getExclusiveAllies())); }; m.GameState.prototype.getAllyStructures = function(allyID) { if (allyID == undefined) return this.updatingCollection("diplo-ally-structures", m.Filters.byOwners(this.getAllies()), this.getStructures()); return this.updatingGlobalCollection("player-" + allyID + "-structures", m.Filters.byOwner(allyID), this.getStructures()); }; m.GameState.prototype.getNeutralStructures = function() { return this.getStructures().filter(m.Filters.byOwners(this.getNeutrals())); }; m.GameState.prototype.getEnemyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getEnemies())); }; m.GameState.prototype.getEnemyStructures = function(enemyID) { if (enemyID === undefined) return this.updatingCollection("diplo-enemy-structures", m.Filters.byOwners(this.getEnemies()), this.getStructures()); return this.updatingGlobalCollection("player-" + enemyID + "-structures", m.Filters.byOwner(enemyID), this.getStructures()); }; m.GameState.prototype.getEnemyUnits = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Unit")); return this.updatingGlobalCollection("player-" + enemyID + "-units", m.Filters.byClass("Unit"), this.getEntities(enemyID)); }; /** if maintain is true, this will be stored. Otherwise it's one-shot. */ m.GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain) { if (maintain === true) return this.updatingCollection(key + "-" + value, m.Filters.byMetadata(this.player, key, value),this.getOwnEntities()); return this.getOwnEntities().filter(m.Filters.byMetadata(this.player, key, value)); }; m.GameState.prototype.getOwnEntitiesByRole = function(role, maintain) { return this.getOwnEntitiesByMetadata("role", role, maintain); }; m.GameState.prototype.getOwnEntitiesByType = function(type, maintain) { let filter = m.Filters.byType(type); if (maintain === true) return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnEntitiesByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("class-" + cls, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnFoundationsByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("foundations-class-" + cls, filter, this.getOwnFoundations()); return this.getOwnFoundations().filter(filter); }; m.GameState.prototype.getOwnTrainingFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities()); }; m.GameState.prototype.getOwnResearchFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-research-facilities", m.Filters.byResearchAvailable(this, this.playerData.civ), this.getOwnEntities()); }; m.GameState.prototype.countEntitiesByType = function(type, maintain) { return this.getOwnEntitiesByType(type, maintain).length; }; m.GameState.prototype.countEntitiesAndQueuedByType = function(type, maintain) { let template = this.getTemplate(type); if (!template) return 0; let count = this.countEntitiesByType(type, maintain); // Count building foundations if (template.hasClass("Structure") === true) count += this.countFoundationsByType(type, true); else if (template.resourceSupplyType() !== undefined) // animal resources count += this.countEntitiesByType("resource|" + type, true); else { // Count entities in building production queues // TODO: maybe this fails for corrals. this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.unitTemplate == type) count += item.count; }); } return count; }; m.GameState.prototype.countFoundationsByType = function(type, maintain) { let foundationType = "foundation|" + type; if (maintain === true) return this.updatingCollection("foundation-type-" + type, m.Filters.byType(foundationType), this.getOwnFoundations()).length; let count = 0; this.getOwnStructures().forEach(function(ent) { if (ent.templateName() == foundationType) ++count; }); return count; }; m.GameState.prototype.countOwnEntitiesByRole = function(role) { return this.getOwnEntitiesByRole(role, "true").length; }; m.GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { let count = this.countOwnEntitiesByRole(role); // Count entities in building production queues this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata.role && item.metadata.role == role) count += item.count; }); return count; }; m.GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { // Count entities in building production queues let count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); return count; }; m.GameState.prototype.getOwnFoundations = function() { return this.updatingGlobalCollection("player-" + this.player + "-foundations", m.Filters.isFoundation(), this.getOwnStructures()); }; m.GameState.prototype.getOwnDropsites = function(resource) { if (resource) return this.updatingCollection("ownDropsite-" + resource, m.Filters.isDropsite(resource), this.getOwnEntities()); return this.updatingCollection("ownDropsite-all", m.Filters.isDropsite(), this.getOwnEntities()); }; m.GameState.prototype.getAnyDropsites = function(resource) { if (resource) return this.updatingGlobalCollection("anyDropsite-" + resource, m.Filters.isDropsite(resource), this.getEntities()); return this.updatingGlobalCollection("anyDropsite-all", m.Filters.isDropsite(), this.getEntities()); }; m.GameState.prototype.getResourceSupplies = function(resource) { return this.updatingGlobalCollection("resource-" + resource, m.Filters.byResource(resource), this.getEntities()); }; m.GameState.prototype.getHuntableSupplies = function() { return this.updatingGlobalCollection("resource-hunt", m.Filters.isHuntable(), this.getEntities()); }; m.GameState.prototype.getFishableSupplies = function() { return this.updatingGlobalCollection("resource-fish", m.Filters.isFishable(), this.getEntities()); }; /** This returns only units from buildings. */ m.GameState.prototype.findTrainableUnits = function(classes, anticlasses) { let allTrainable = []; let civ = this.playerData.civ; this.getOwnTrainingFacilities().forEach(function(ent) { let trainable = ent.trainableEntities(civ); if (!trainable) return; for (let unit of trainable) if (allTrainable.indexOf(unit) === -1) allTrainable.push(unit); }); let ret = []; let limits = this.getEntityLimits(); let current = this.getEntityCounts(); for (let trainable of allTrainable) { if (this.isTemplateDisabled(trainable)) continue; let template = this.getTemplate(trainable); if (!template || !template.available(this)) continue; if (classes.some(c => !template.hasClass(c))) continue; if (anticlasses.some(c => template.hasClass(c))) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; ret.push( [trainable, template] ); } return ret; }; /** * Return all techs which can currently be researched * Does not factor cost. * If there are pairs, both techs are returned. */ m.GameState.prototype.findAvailableTech = function() { let allResearchable = []; let civ = this.playerData.civ; for (let ent of this.getOwnEntities().values()) { let searchable = ent.researchableTechs(this, civ); if (!searchable) continue; for (let tech of searchable) if (!this.playerData.disabledTechnologies[tech] && allResearchable.indexOf(tech) === -1) allResearchable.push(tech); } let ret = []; for (let tech of allResearchable) { let template = this.getTemplate(tech); if (template.pairDef()) { let techs = template.getPairedTechs(); if (this.canResearch(techs[0]._templateName)) ret.push([techs[0]._templateName, techs[0]] ); if (this.canResearch(techs[1]._templateName)) ret.push([techs[1]._templateName, techs[1]] ); } else if (this.canResearch(tech)) { // Phases are treated separately if (this.phases.every(phase => template._templateName != phase.name)) ret.push( [tech, template] ); } } return ret; }; /** * Return true if we have a building able to train that template */ m.GameState.prototype.hasTrainer = function(template) { let civ = this.playerData.civ; for (let ent of this.getOwnTrainingFacilities().values()) { let trainable = ent.trainableEntities(civ); if (trainable && trainable.indexOf(template) !== -1) return true; } return false; }; /** * Find buildings able to train that template. */ m.GameState.prototype.findTrainers = function(template) { let civ = this.playerData.civ; return this.getOwnTrainingFacilities().filter(function(ent) { let trainable = ent.trainableEntities(civ); return trainable && trainable.indexOf(template) !== -1; }); }; /** * Get any unit that is capable of constructing the given building type. */ m.GameState.prototype.findBuilder = function(template) { let civ = this.getPlayerCiv(); for (let ent of this.getOwnUnits().values()) { let buildable = ent.buildableEntities(civ); if (buildable && buildable.indexOf(template) !== -1) return ent; } return undefined; }; /** Return true if one of our buildings is capable of researching the given tech */ m.GameState.prototype.hasResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return false; let civ = this.playerData.civ; for (let ent of this.getOwnResearchFacilities().values()) { let techs = ent.researchableTechs(this, civ); for (let tech of techs) { let temp = this.getTemplate(tech); if (temp.pairDef()) { let pairedTechs = temp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } } return false; }; /** Find buildings that are capable of researching the given tech */ m.GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return undefined; let self = this; let civ = this.playerData.civ; return this.getOwnResearchFacilities().filter(function(ent) { let techs = ent.researchableTechs(self, civ); for (let tech of techs) { let thisTemp = self.getTemplate(tech); if (thisTemp.pairDef()) { let pairedTechs = thisTemp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } return false; }); }; m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; m.GameState.prototype.isTemplateAvailable = function(templateName) { if (this.templates[templateName] === undefined) this.sharedScript.GetTemplate(templateName); return this.templates[templateName] && !this.isTemplateDisabled(templateName); }; m.GameState.prototype.isTemplateDisabled = function(templateName) { if (!this.playerData.disabledTemplates[templateName]) return false; return this.playerData.disabledTemplates[templateName]; }; /** Checks whether the maximum number of buildings have been constructed for a certain catergory */ m.GameState.prototype.isEntityLimitReached = function(category) { if (this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined) return false; return this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]; }; m.GameState.prototype.getTraderTemplatesGains = function() { let shipMechantTemplateName = this.applyCiv("units/{civ}_ship_merchant"); let supportTraderTemplateName = this.applyCiv("units/{civ}_support_trader"); let shipMerchantTemplate = !this.isTemplateDisabled(shipMechantTemplateName) && this.getTemplate(shipMechantTemplateName); let supportTraderTemplate = !this.isTemplateDisabled(supportTraderTemplateName) && this.getTemplate(supportTraderTemplateName); let norm = TradeGainNormalization(this.sharedScript.mapSize); let ret = {}; if (supportTraderTemplate) ret.landGainMultiplier = norm * supportTraderTemplate.gainMultiplier(); if (shipMerchantTemplate) ret.navalGainMultiplier = norm * shipMerchantTemplate.gainMultiplier(); return ret; }; return m; }(API3);