Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 27041) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 27042) @@ -1,633 +1,638 @@ /** * 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", "Music", "CivBonuses", "StartEntities", "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) continue; const template = Engine.GetTemplate("special/players/" + data.Code); data.Name = template.Identity.GenericName; data.Emblem = "session/portraits/" + template.Identity.Icon; data.History = template.Identity.History; civData[data.Code] = data; } return civData; } /** * @return {string[]} - All the classes for this identity template. */ function GetIdentityClasses(template) { let classString = ""; if (template.Classes && template.Classes._string) classString += " " + template.Classes._string; if (template.VisibleClasses && template.VisibleClasses._string) classString += " " + template.VisibleClasses._string; if (template.Rank) classString += " " + template.Rank; return classString.length > 1 ? classString.substring(1).split(" ") : []; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : []; } /** * 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. + * @param {number} default_value - A value to use if one is not specified in the template. * @return {number} */ -function GetBaseTemplateDataValue(template, value_path) +function GetBaseTemplateDataValue(template, value_path, default_value) { let current_value = template; for (let property of value_path.split("/")) - current_value = current_value[property] || 0; + current_value = current_value[property] || default_value; 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. + * @param {number} default_value - A value to use if one is not specified in the template. * @return {number} Modifier altered value. */ -function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) +function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}, default_value) { - let current_value = GetBaseTemplateDataValue(template, value_path); + let current_value = GetBaseTemplateDataValue(template, value_path, default_value); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers && modifiers[mod_key]) current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), 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 class. * @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, modifiers = {}) +function GetTemplateDataHelper(template, player, auraTemplates, resources, 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); + // @param {number} default_value - A value to use if one is not specified in the template. + const getEntityValue = function(value_path, mod_key, default_value = 0) { + return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers, default_value); }; let ret = {}; if (template.Resistance) { // Don't show Foundation resistance. ret.resistance = {}; if (template.Resistance.Entity) { if (template.Resistance.Entity.Damage) { ret.resistance.Damage = {}; for (let damageType in template.Resistance.Entity.Damage) ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType); } if (template.Resistance.Entity.Capture) ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture"); if (template.Resistance.Entity.ApplyStatus) { ret.resistance.ApplyStatus = {}; for (let statusEffect in template.Resistance.Entity.ApplyStatus) ret.resistance.ApplyStatus[statusEffect] = { "blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"), "duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration") }; } } } let getAttackEffects = (temp, path) => { let effects = {}; if (temp.Capture) effects.Capture = getEntityValue(path + "/Capture"); if (temp.Damage) { effects.Damage = {}; for (let damageType in temp.Damage) effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType); } if (temp.ApplyStatus) effects.ApplyStatus = temp.ApplyStatus; return effects; }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; ret.attack[type] = { "attackName": { "name": template.Attack[type].AttackName._string || template.Attack[type].AttackName, "context": template.Attack[type].AttackName["@context"] }, "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "yOrigin": getAttackStat("Origin/Y") }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].yOrigin + ret.attack[type].maxRange)); ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Projectile) ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true"; Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); if (template.Attack[type].Splash) { ret.attack[type].splash = { "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, }; Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", }; Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) ret.auras[auraID] = GetAuraDataHelper(auraTemplates[auraID]); } 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.TrainingRestrictions.MatchLimit) ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit; } 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.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.Garrisonable) ret.garrisonable = { "size": getEntityValue("Garrisonable/Size") }; if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "health": getEntityValue("Heal/Health"), "range": getEntityValue("Heal/Range"), "interval": getEntityValue("Heal/Interval") }; 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.ResourceDropsite) ret.resourceDropsite = { "types": template.ResourceDropsite.Types.split(" ") }; 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.Population && template.Population.Bonus) ret.population = { "bonus": getEntityValue("Population/Bonus") }; 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); ret.nativeCiv = template.Identity.Civ; } if (template.UnitMotion) { const walkSpeed = getEntityValue("UnitMotion/WalkSpeed"); ret.speed = { "walk": walkSpeed, "run": walkSpeed, "acceleration": getEntityValue("UnitMotion/Acceleration") }; if (template.UnitMotion.RunMultiplier) ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier"); } 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.Researcher) { ret.techCostMultiplier = {}; - for (const res in template.Researcher.TechCostMultiplier) - ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res); + for (const res of resources.GetCodes().concat(["time"])) + ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res, null, 1); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.Treasure) { ret.treasure = { "collectTime": getEntityValue("Treasure/CollectTime"), "resources": {} }; for (let resource in template.Treasure.Resources) ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource); } if (template.TurretHolder) ret.turretHolder = { "turretPoints": template.TurretHolder.TurretPoints }; if (template.Upkeep) { ret.upkeep = { "interval": +template.Upkeep.Interval, "rates": {} }; for (let type in template.Upkeep.Rates) ret.upkeep.rates[type] = getEntityValue("Upkeep/Rates/" + type); } 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(/\s+/); } 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, "replaces": template.replaces }; } /** * Get information about a technology template. * @param {Object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. + * @param {Object} resources - An instance of the Resources class. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } /** * Get information about an aura template. * @param {object} template - A valid template as obtained by loading the aura JSON file. */ function GetAuraDataHelper(template) { return { "name": { "generic": template.auraName, }, "description": template.auraDescription || null, "modifications": template.modifications, "radius": template.radius || null, }; } 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; } /** * Remove filter prefix (mirage, corpse, etc) from template name. * * ie. filter|dir/to/template -> dir/to/template */ function removeFiltersFromTemplateName(templateName) { return templateName.split("|").pop(); } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 27041) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 27042) @@ -1,394 +1,394 @@ /** * This class parses and stores parsed template data. */ class TemplateParser { constructor(TemplateLoader) { this.TemplateLoader = TemplateLoader; /** * Parsed Data Stores */ this.auras = {}; this.entities = {}; this.techs = {}; this.phases = {}; this.modifiers = {}; this.players = {}; this.phaseList = []; } getAura(auraName) { if (auraName in this.auras) return this.auras[auraName]; if (!AuraTemplateExists(auraName)) return null; let template = this.TemplateLoader.loadAuraTemplate(auraName); let parsed = GetAuraDataHelper(template); if (template.civ) parsed.civ = template.civ; let affectedPlayers = template.affectedPlayers || this.AuraAffectedPlayerDefault; parsed.affectsTeam = this.AuraTeamIndicators.some(indicator => affectedPlayers.includes(indicator)); parsed.affectsSelf = this.AuraSelfIndicators.some(indicator => affectedPlayers.includes(indicator)); this.auras[auraName] = parsed; return this.auras[auraName]; } /** * Load and parse a structure, unit, resource, etc from its entity template file. * * @param {string} templateName * @param {string} civCode * @return {(object|null)} Sanitized object about the requested template or null if entity template doesn't exist. */ getEntity(templateName, civCode) { if (!(civCode in this.entities)) this.entities[civCode] = {}; else if (templateName in this.entities[civCode]) return this.entities[civCode][templateName]; if (!Engine.TemplateExists(templateName)) return null; let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); - let parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, this.modifiers[civCode] || {}); + const parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {}); parsed.name.internal = templateName; parsed.history = template.Identity.History; parsed.production = this.TemplateLoader.deriveProduction(template, civCode); if (template.Builder) parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode); // Set the minimum phase that this entity is available. // For gaia objects, this is meaningless. if (!parsed.requiredTechnology) parsed.phase = this.phaseList[0]; else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology)) parsed.phase = this.getActualPhase(parsed.requiredTechnology); else parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode); if (template.Identity.Rank) parsed.promotion = { "current_rank": template.Identity.Rank, "entity": template.Promotion && template.Promotion.Entity }; if (template.ResourceSupply) parsed.supply = { "type": template.ResourceSupply.Type.split("."), "amount": template.ResourceSupply.Max, }; if (parsed.upgrades) parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode); if (parsed.wallSet) { parsed.wallset = {}; if (!parsed.upgrades) parsed.upgrades = []; // Note: An assumption is made here that wall segments all have the same resistance and auras let struct = this.getEntity(parsed.wallSet.templates.long, civCode); parsed.resistance = struct.resistance; parsed.auras = struct.auras; // For technology cost multiplier, we need to use the tower struct = this.getEntity(parsed.wallSet.templates.tower, civCode); parsed.techCostMultiplier = struct.techCostMultiplier; let health; for (let wSegm in parsed.wallSet.templates) { if (wSegm == "fort" || wSegm == "curves") continue; let wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode); parsed.wallset[wSegm] = wPart; for (let research of wPart.production.techs) parsed.production.techs.push(research); if (wPart.upgrades) Array.prototype.push.apply(parsed.upgrades, 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 (parsed.wallSet.templates.curves) for (let curve of parsed.wallSet.templates.curves) { let wPart = this.getEntity(curve, civCode); health.min = Math.min(health.min, wPart.health); health.max = Math.max(health.max, wPart.health); } if (health.min == health.max) parsed.health = health.min; else parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), { "health_min": health.min, "health_max": health.max }); } this.entities[civCode][templateName] = parsed; return parsed; } /** * Load and parse technology from json template. * * @param {string} technologyName * @param {string} civCode * @return {Object} Sanitized data about the requested technology. */ getTechnology(technologyName, civCode) { if (!TechnologyTemplateExists(technologyName)) return null; if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases) return this.phases[technologyName]; if (!(civCode in this.techs)) this.techs[civCode] = {}; else if (technologyName in this.techs[civCode]) return this.techs[civCode][technologyName]; let template = this.TemplateLoader.loadTechnologyTemplate(technologyName); - let tech = GetTechnologyDataHelper(template, civCode, g_ResourceData); + const tech = GetTechnologyDataHelper(template, civCode, g_ResourceData, this.modifiers[civCode] || {}); tech.name.internal = technologyName; if (template.pair !== undefined) { tech.pair = template.pair; tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs); } if (this.TemplateLoader.isPhaseTech(technologyName)) { tech.actualPhase = technologyName; if (tech.replaces !== undefined) tech.actualPhase = tech.replaces[0]; this.phases[technologyName] = tech; } else this.techs[civCode][technologyName] = tech; return tech; } /** * @param {string} phaseCode * @param {string} civCode * @return {Object} Sanitized object containing phase data */ getPhase(phaseCode, civCode) { return this.getTechnology(phaseCode, civCode); } /** * Load and parse the relevant player_{civ}.xml template. */ getPlayer(civCode) { if (civCode in this.players) return this.players[civCode]; let template = this.TemplateLoader.loadPlayerTemplate(civCode); let parsed = { "civbonuses": [], "teambonuses": [], }; if (template.Auras) for (let auraTemplateName of template.Auras._string.split(/\s+/)) if (AuraTemplateExists(auraTemplateName)) if (this.getAura(auraTemplateName).affectsTeam) parsed.teambonuses.push(auraTemplateName); else parsed.civbonuses.push(auraTemplateName); this.players[civCode] = parsed; return parsed; } /** * 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. */ getActualUpgradeData(upgradesInfo, civCode) { let newUpgrades = []; for (let upgrade of upgradesInfo) { upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode); - let data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData, this.modifiers[civCode] || {}); + const data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {}); data.name.internal = upgrade.entity; data.cost = upgrade.cost; data.icon = upgrade.icon || data.icon; data.tooltip = upgrade.tooltip || data.tooltip; data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology; if (!data.requiredTechnology) data.phase = this.phaseList[0]; else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology)) data.phase = this.getActualPhase(data.requiredTechnology); else data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode); 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 * @param {string} civCode * @return The name of the phase the technology belongs to, or false if * the current civ can't research this tech */ getPhaseOfTechnology(techName, civCode) { let phaseIdx = -1; if (basename(techName).startsWith("phase")) { if (!this.phases[techName].reqs) return false; phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName)); if (phaseIdx > 0) return this.phaseList[phaseIdx - 1]; } let techReqs = this.getTechnology(techName, civCode).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, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode))); } return this.phaseList[phaseIdx] || false; } /** * Returns the actual phase a certain phase tech represents or stands in for. * * For example, passing `phase_city_athen` would result in `phase_city`. * * @param {string} phaseName * @return {string} */ getActualPhase(phaseName) { if (this.phases[phaseName]) return this.phases[phaseName].actualPhase; warn("Unrecognized phase (" + phaseName + ")"); return this.phaseList[0]; } getModifiers(civCode) { return this.modifiers[civCode]; } deriveModifications(civCode) { const player = this.getPlayer(civCode); const auraList = clone(player.civbonuses); for (const bonusname of player.teambonuses) if (this.getAura(bonusname).affectsSelf) auraList.push(bonusname); this.modifiers[civCode] = this.TemplateLoader.deriveModifications(civCode, auraList); } derivePhaseList(technologyList, civCode) { // Load all of a civ's specific phase technologies for (let techcode of technologyList) if (this.TemplateLoader.isPhaseTech(techcode)) this.getTechnology(techcode, civCode); this.phaseList = UnravelPhases(this.phases); // Make sure all required generic phases are loaded and parsed for (let phasecode of this.phaseList) this.getTechnology(phasecode, civCode); } mergeRequirements(reqsA, reqsB) { if (!reqsA || !reqsB) 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] = []; Array.prototype.push.apply(finalReqs[opt][type], option[type]); } return finalReqs; } } // Default affected player token list to use if an aura doesn't explicitly give one. // Keep in sync with simulation/components/Auras.js TemplateParser.prototype.AuraAffectedPlayerDefault = ["Player"]; // List of tokens that, if found in an aura's "affectedPlayers" attribute, indicate // that the aura applies to team members. TemplateParser.prototype.AuraTeamIndicators = ["MutualAlly", "ExclusiveMutualAlly"]; // List of tokens that, if found in an aura's "affectedPlayers" attribute, indicate // that the aura applies to the aura's owning civ. TemplateParser.prototype.AuraSelfIndicators = ["Player", "Ally", "MutualAlly"]; Index: ps/trunk/binaries/data/mods/public/gui/reference/common/common.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/common.js (revision 27041) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/common.js (revision 27042) @@ -1,33 +1,33 @@ /** * This needs to stay in the global scope, as it is used by various functions * within gui/common/tooltip.js */ var g_ResourceData = new Resources(); var g_Page; /** * 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 the TemplateLoader * class already. */ function GetTemplateData(templateName) { let template = g_Page.TemplateLoader.loadEntityTemplate(templateName, g_Page.activeCiv); - return GetTemplateDataHelper(template, null, g_Page.TemplateLoader.auraData, g_Page.TemplateParser.getModifiers(g_Page.activeCiv)); + return GetTemplateDataHelper(template, null, g_Page.TemplateLoader.auraData, g_ResourceData, g_Page.TemplateParser.getModifiers(g_Page.activeCiv)); } /** * This would ideally be an Engine method. * Or part of globalscripts. Either would be better than here. */ function TechnologyTemplateExists(templateName) { return Engine.FileExists(g_Page.TemplateLoader.TechnologyPath + templateName + ".json"); } function AuraTemplateExists(templateName) { return Engine.FileExists(g_Page.TemplateLoader.AuraPath + templateName + ".json"); } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 27041) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 27042) @@ -1,935 +1,943 @@ /** * @file Contains functionality to place walls on random maps. */ /** + * Provide the bare minimum so we can load templates without error. + * We don't actually need to know the actual resource codes. + */ +const g_Resources = { + "GetCodes": () => [], +}; + +/** * Set some globals for this module. */ var g_WallStyles = loadWallsetsFromCivData(); var g_FortressTypes = createDefaultFortressTypes(); /** * Fetches wallsets from {civ}.json files, and then uses them to load * basic wall elements. */ function loadWallsetsFromCivData() { let wallsets = {}; for (let civ in g_CivData) { let civInfo = g_CivData[civ]; if (!civInfo.WallSets) continue; for (let path of civInfo.WallSets) { // File naming conventions: // - structures/wallset_{style} // - structures/{civ}/wallset_{style} let style = basename(path).split("_")[1]; if (path.split("/").indexOf(civ) != -1) style = civ + "/" + style; if (!wallsets[style]) wallsets[style] = loadWallset(Engine.GetTemplate(path), civ); } } return wallsets; } function loadWallset(wallsetPath, civ) { let newWallset = { "curves": [] }; - let wallsetData = GetTemplateDataHelper(wallsetPath).wallSet; + const wallsetData = GetTemplateDataHelper(wallsetPath, null, null, g_Resources).wallSet; for (let element in wallsetData.templates) if (element == "curves") for (let filename of wallsetData.templates.curves) newWallset.curves.push(readyWallElement(filename, civ)); else newWallset[element] = readyWallElement(wallsetData.templates[element], civ); newWallset.overlap = wallsetData.minTowerOverlap * newWallset.tower.length; return newWallset; } /** * Fortress class definition * * We use "fortress" to describe a closed wall built of multiple wall * elements attached together surrounding a central point. We store the * abstract of the wall (gate, tower, wall, ...) and only apply the style * when we get to build it. * * @param {string} type - Descriptive string, example: "tiny". Not really needed (WallTool.wallTypes["type string"] is used). Mainly for custom wall elements. * @param {array} [wall] - Array of wall element strings. May be defined at a later point. * Example: ["medium", "cornerIn", "gate", "cornerIn", "medium", "cornerIn", "gate", "cornerIn"] * @param {Object} [centerToFirstElement] - Vector from the visual center of the fortress to the first wall element. * @param {number} [centerToFirstElement.x] * @param {number} [centerToFirstElement.y] */ function Fortress(type, wall=[], centerToFirstElement=undefined) { this.type = type; this.wall = wall; this.centerToFirstElement = centerToFirstElement; } function createDefaultFortressTypes() { let defaultFortresses = {}; /** * Define some basic default fortress types. */ let addFortress = (type, walls) => defaultFortresses[type] = { "wall": walls.concat(walls, walls, walls) }; addFortress("tiny", ["gate", "tower", "short", "cornerIn", "short", "tower"]); addFortress("small", ["gate", "tower", "medium", "cornerIn", "medium", "tower"]); addFortress("medium", ["gate", "tower", "long", "cornerIn", "long", "tower"]); addFortress("normal", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "medium", "cornerIn", "medium", "tower"]); addFortress("large", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]); addFortress("veryLarge", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "long", "cornerIn", "long", "cornerOut", "medium", "cornerIn", "medium", "tower"]); addFortress("giant", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]); /** * Define some fortresses based on those above, but designed for use * with the "palisades" wallset. */ for (let fortressType in defaultFortresses) { const fillTowersBetween = ["short", "medium", "long", "start", "end", "cornerIn", "cornerOut"]; const newKey = fortressType + "Palisades"; const oldWall = defaultFortresses[fortressType].wall; defaultFortresses[newKey] = { "wall": [] }; for (let j = 0; j < oldWall.length; ++j) { defaultFortresses[newKey].wall.push(oldWall[j]); if (j + 1 < oldWall.length && fillTowersBetween.indexOf(oldWall[j]) != -1 && fillTowersBetween.indexOf(oldWall[j + 1]) != -1) { defaultFortresses[newKey].wall.push("tower"); } } } return defaultFortresses; } /** * Define some helper functions */ /** * Get a wall element of a style. * * Valid elements: * long, medium, short, start, end, cornerIn, cornerOut, tower, fort, gate, entry, entryTower, entryFort * * Dynamic elements: * `gap_{x}` returns a non-blocking gap of length `x` meters. * `turn_{x}` returns a zero-length turn of angle `x` radians. * * Any other arbitrary string passed will be attempted to be used as: `structures/{civ}/{arbitrary_string}`. * * @param {string} element - What sort of element to fetch. * @param {string} [style] - The style from which this element should come from. * @returns {Object} The wall element requested. Or a tower element. */ function getWallElement(element, style) { style = validateStyle(style); if (g_WallStyles[style][element]) return g_WallStyles[style][element]; // Attempt to derive any unknown elements. // Defaults to a wall tower piece const quarterBend = Math.PI / 2; let wallset = g_WallStyles[style]; let civ = style.split("/")[0]; let ret = wallset.tower ? clone(wallset.tower) : { "angle": 0, "bend": 0, "length": 0, "indent": 0 }; switch (element) { case "cornerIn": if (wallset.curves) for (let curve of wallset.curves) if (curve.bend == quarterBend) ret = curve; if (ret.bend != quarterBend) { ret.angle += Math.PI / 4; ret.indent = ret.length / 4; ret.length = 0; ret.bend = Math.PI / 2; } break; case "cornerOut": if (wallset.curves) for (let curve of wallset.curves) if (curve.bend == quarterBend) { ret = clone(curve); ret.angle += Math.PI / 2; ret.indent -= ret.indent * 2; } if (ret.bend != quarterBend) { ret.angle -= Math.PI / 4; ret.indent = -ret.length / 4; ret.length = 0; } ret.bend = -Math.PI / 2; break; case "entry": ret.templateName = undefined; ret.length = wallset.gate.length; break; case "entryTower": ret.templateName = g_CivData[civ] ? "structures/" + civ + "/defense_tower" : "structures/palisades_watchtower"; ret.indent = ret.length * -3; ret.length = wallset.gate.length; break; case "entryFort": ret = clone(wallset.fort); ret.angle -= Math.PI; ret.length *= 1.5; ret.indent = ret.length; break; case "start": if (wallset.end) { ret = clone(wallset.end); ret.angle += Math.PI; } break; case "end": if (wallset.end) ret = wallset.end; break; default: if (element.startsWith("gap_")) { ret.templateName = undefined; ret.angle = 0; ret.length = +element.slice("gap_".length); } else if (element.startsWith("turn_")) { ret.templateName = undefined; ret.bend = +element.slice("turn_".length) * Math.PI; ret.length = 0; } else { if (!g_CivData[civ]) civ = Object.keys(g_CivData)[0]; let templateName = "structures/" + civ + "/" + element; if (Engine.TemplateExists(templateName)) { ret.indent = ret.length * (element == "outpost" || element.endsWith("_tower") ? -3 : 3.5); ret.templateName = templateName; ret.length = 0; } else warn("Unrecognised wall element: '" + element + "' (" + style + "). Defaulting to " + (wallset.tower ? "'tower'." : "a blank element.")); } } // Cache to save having to calculate this element again. g_WallStyles[style][element] = deepfreeze(ret); return ret; } /** * Prepare a wall element for inclusion in a style. * * @param {string} path - The template path to read values from */ function readyWallElement(path, civCode) { path = path.replace(/\{civ\}/g, civCode); - let template = GetTemplateDataHelper(Engine.GetTemplate(path), null, null); + const template = GetTemplateDataHelper(Engine.GetTemplate(path), null, null, g_Resources); let length = template.wallPiece ? template.wallPiece.length : template.obstruction.shape.width; return deepfreeze({ "templateName": path, "angle": template.wallPiece ? template.wallPiece.angle : Math.PI, "length": length / TERRAIN_TILE_SIZE, "indent": template.wallPiece ? template.wallPiece.indent / TERRAIN_TILE_SIZE : 0, "bend": template.wallPiece ? template.wallPiece.bend : 0 }); } /** * Returns a list of objects containing all information to place all the wall elements entities with placeObject (but the player ID) * Placing the first wall element at startX/startY placed with an angle given by orientation * An alignment can be used to get the "center" of a "wall" (more likely used for fortresses) with getCenterToFirstElement * * @param {Vector2D} position * @param {array} [wall] * @param {string} [style] * @param {number} [orientation] * @returns {array} */ function getWallAlignment(position, wall = [], style = "athen_stone", orientation = 0) { style = validateStyle(style); let alignment = []; let wallPosition = position.clone(); for (let i = 0; i < wall.length; ++i) { let element = getWallElement(wall[i], style); if (!element && i == 0) { warn("Not a valid wall element: style = " + style + ", wall[" + i + "] = " + wall[i] + "; " + uneval(element)); continue; } // Add wall elements entity placement arguments to the alignment alignment.push({ "position": Vector2D.sub(wallPosition, new Vector2D(element.indent, 0).rotate(-orientation)), "templateName": element.templateName, "angle": orientation + element.angle }); // Preset vars for the next wall element if (i + 1 < wall.length) { orientation += element.bend; let nextElement = getWallElement(wall[i + 1], style); if (!nextElement) { warn("Not a valid wall element: style = " + style + ", wall[" + (i + 1) + "] = " + wall[i + 1] + "; " + uneval(nextElement)); continue; } let distance = (element.length + nextElement.length) / 2 - g_WallStyles[style].overlap; // Corrections for elements with indent AND bending let indent = element.indent; let bend = element.bend; if (bend != 0 && indent != 0) { // Indent correction to adjust distance distance += indent * Math.sin(bend); // Indent correction to normalize indentation wallPosition.add(new Vector2D(indent).rotate(-orientation)); } // Set the next coordinates of the next element in the wall without indentation adjustment wallPosition.add(new Vector2D(distance, 0).rotate(-orientation).perpendicular()); } } return alignment; } /** * Center calculation works like getting the center of mass assuming all wall elements have the same "weight" * * Used to get centerToFirstElement of fortresses by default * * @param {number} alignment * @returns {Object} Vector from the center of the set of aligned wallpieces to the first wall element. */ function getCenterToFirstElement(alignment) { return alignment.reduce((result, align) => result.sub(Vector2D.div(align.position, alignment.length)), new Vector2D(0, 0)); } /** * Does not support bending wall elements like corners. * * @param {string} style * @param {array} wall * @returns {number} The sum length (in terrain cells, not meters) of the provided wall. */ function getWallLength(style, wall) { style = validateStyle(style); let length = 0; let overlap = g_WallStyles[style].overlap; for (let element of wall) length += getWallElement(element, style).length - overlap; return length; } /** * Makes sure the style exists and, if not, provides a fallback. * * @param {string} style * @param {number} [playerId] * @returns {string} Valid style. */ function validateStyle(style, playerId = 0) { if (!style || !g_WallStyles[style]) { if (playerId == 0) return Object.keys(g_WallStyles)[0]; style = getCivCode(playerId) + "/stone"; return !g_WallStyles[style] ? Object.keys(g_WallStyles)[0] : style; } return style; } /** * Define the different wall placer functions */ /** * Places an abitrary wall beginning at the location comprised of the array of elements provided. * * @param {Vector2D} position * @param {array} [wall] - Array of wall element types. Example: ["start", "long", "tower", "long", "end"] * @param {string} [style] - Wall style string. * @param {number} [playerId] - Identifier of the player for whom the wall will be placed. * @param {number} [orientation] - Angle at which the first wall element is placed. * 0 means "outside" or "front" of the wall is right (positive X) like placeObject * It will then be build towards top/positive Y (if no bending wall elements like corners are used) * Raising orientation means the wall is rotated counter-clockwise like placeObject */ function placeWall(position, wall = [], style, playerId = 0, orientation = 0, constraints = undefined) { style = validateStyle(style, playerId); let entities = []; let constraint = new StaticConstraint(constraints); for (let align of getWallAlignment(position, wall, style, orientation)) if (align.templateName && g_Map.inMapBounds(align.position) && constraint.allows(align.position.clone().floor())) entities.push(g_Map.placeEntityPassable(align.templateName, playerId, align.position, align.angle)); return entities; } /** * Places an abitrarily designed "fortress" (closed loop of wall elements) * centered around a given point. * * The fortress wall should always start with the main entrance (like * "entry" or "gate") to get the orientation correct. * * @param {Vector2D} centerPosition * @param {Object} [fortress] - If not provided, defaults to the predefined "medium" fortress type. * @param {string} [style] - Wall style string. * @param {number} [playerId] - Identifier of the player for whom the wall will be placed. * @param {number} [orientation] - Angle the first wall element (should be a gate or entrance) is placed. Default is 0 */ function placeCustomFortress(centerPosition, fortress, style, playerId = 0, orientation = 0, constraints = undefined) { fortress = fortress || g_FortressTypes.medium; style = validateStyle(style, playerId); // Calculate center if fortress.centerToFirstElement is undefined (default) let centerToFirstElement = fortress.centerToFirstElement; if (centerToFirstElement === undefined) centerToFirstElement = getCenterToFirstElement(getWallAlignment(new Vector2D(0, 0), fortress.wall, style)); // Placing the fortress wall let position = Vector2D.sum([ centerPosition, new Vector2D(centerToFirstElement.x, 0).rotate(-orientation), new Vector2D(centerToFirstElement.y, 0).perpendicular().rotate(-orientation) ]); return placeWall(position, fortress.wall, style, playerId, orientation, constraints); } /** * Places a predefined fortress centered around the provided point. * * @see Fortress * * @param {string} [type] - Predefined fortress type, as used as a key in g_FortressTypes. */ function placeFortress(centerPosition, type = "medium", style, playerId = 0, orientation = 0, constraints = undefined) { return placeCustomFortress(centerPosition, g_FortressTypes[type], style, playerId, orientation, constraints); } /** * Places a straight wall from a given point to another, using the provided * wall parts repeatedly. * * Note: Any "bending" wall pieces passed will be complained about. * * @param {Vector2D} startPosition - Approximate start point of the wall. * @param {Vector2D} targetPosition - Approximate end point of the wall. * @param {array} [wallPart=["tower", "long"]] * @param {number} [playerId] * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. */ function placeLinearWall(startPosition, targetPosition, wallPart = undefined, style, playerId = 0, endWithFirst = true, constraints = undefined) { wallPart = wallPart || ["tower", "long"]; style = validateStyle(style, playerId); // Check arguments for (let element of wallPart) if (getWallElement(element, style).bend != 0) warn("placeLinearWall : Bending is not supported by this function, but the following bending wall element was used: " + element); // Setup number of wall parts let totalLength = startPosition.distanceTo(targetPosition); let wallPartLength = getWallLength(style, wallPart); let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); // Setup scale factor let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); // Setup angle let wallAngle = getAngle(startPosition.x, startPosition.y, targetPosition.x, targetPosition.y); let placeAngle = wallAngle - Math.PI / 2; // Place wall entities let entities = []; let position = startPosition.clone(); let overlap = g_WallStyles[style].overlap; let constraint = new StaticConstraint(constraints); for (let partIndex = 0; partIndex < numParts; ++partIndex) for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) { let wallEle = getWallElement(wallPart[elementIndex], style); let wallLength = (wallEle.length - overlap) / 2; let dist = new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle); // Length correction position.add(dist); // Indent correction let place = Vector2D.add(position, new Vector2D(0, wallEle.indent).rotate(-wallAngle)); if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); position.add(dist); } if (endWithFirst) { let wallEle = getWallElement(wallPart[0], style); let wallLength = (wallEle.length - overlap) / 2; position.add(new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle)); if (wallEle.templateName && g_Map.inMapBounds(position) && constraint.allows(position.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, position, placeAngle + wallEle.angle)); } return entities; } /** * Places a (semi-)circular wall of repeated wall elements around a central * point at a given radius. * * The wall does not have to be closed, and can be left open in the form * of an arc if maxAngle < 2 * Pi. In this case, the orientation determines * where this open part faces, with 0 meaning "right" like an unrotated * building's drop-point. * * Note: Any "bending" wall pieces passed will be complained about. * * @param {Vector2D} center - Center of the circle or arc. * @param (number} radius - Approximate radius of the circle. (Given the maxBendOff argument) * @param {array} [wallPart] * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Angle at which the first wall element is placed. * @param {number} [maxAngle] - How far the wall should circumscribe the center. Default is Pi * 2 (for a full circle). * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. For full circles, the default is false. For arcs, true. * @param {number} [maxBendOff] Optional. How irregular the circle should be. 0 means regular circle, PI/2 means very irregular. Default is 0 (regular circle) */ function placeCircularWall(center, radius, wallPart, style, playerId = 0, orientation = 0, maxAngle = 2 * Math.PI, endWithFirst, maxBendOff = 0, constraints = undefined) { wallPart = wallPart || ["tower", "long"]; style = validateStyle(style, playerId); if (endWithFirst === undefined) endWithFirst = maxAngle < Math.PI * 2 - 0.001; // Can this be done better? // Check arguments if (maxBendOff > Math.PI / 2 || maxBendOff < 0) warn("placeCircularWall : maxBendOff should satisfy 0 < maxBendOff < PI/2 (~1.5rad) but it is: " + maxBendOff); for (let element of wallPart) if (getWallElement(element, style).bend != 0) warn("placeCircularWall : Bending is not supported by this function, but the following bending wall element was used: " + element); // Setup number of wall parts let totalLength = maxAngle * radius; let wallPartLength = getWallLength(style, wallPart); let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); // Setup scale factor let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); // Place wall entities let entities = []; let constraint = new StaticConstraint(constraints); let actualAngle = orientation; let position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle)); let overlap = g_WallStyles[style].overlap; for (let partIndex = 0; partIndex < numParts; ++partIndex) for (let wallEle of wallPart) { wallEle = getWallElement(wallEle, style); // Width correction let addAngle = scaleFactor * (wallEle.length - overlap) / radius; let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle)); let place = Vector2D.average([position, target]); let placeAngle = actualAngle + addAngle / 2; // Indent correction place.sub(new Vector2D(wallEle.indent, 0).rotate(-placeAngle)); // Placement if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) { let entity = g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle); if (entity) entities.push(entity); } // Prepare for the next wall element actualAngle += addAngle; position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle)); } if (endWithFirst) { let wallEle = getWallElement(wallPart[0], style); let addAngle = scaleFactor * wallEle.length / radius; let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle)); let place = Vector2D.average([position, target]); let placeAngle = actualAngle + addAngle / 2; if (g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); } return entities; } /** * Places a polygonal wall of repeated wall elements around a central * point at a given radius. * * Note: Any "bending" wall pieces passed will be ignored. * * @param {Vector2D} centerPosition * @param {number} radius * @param {array} [wallPart] * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners. * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Direction the first wall piece or opening in the wall faces. * @param {number} [numCorners] - How many corners the polygon will have. * @param {boolean} [skipFirstWall] - If the first linear wall part will be left opened as entrance. */ function placePolygonalWall(centerPosition, radius, wallPart, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners = 8, skipFirstWall = true, constraints = undefined) { wallPart = wallPart || ["long", "tower"]; style = validateStyle(style, playerId); let entities = []; let constraint = new StaticConstraint(constraints); let angleAdd = Math.PI * 2 / numCorners; let angleStart = orientation - angleAdd / 2; let corners = new Array(numCorners).fill(0).map((zero, i) => Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleStart - i * angleAdd))); for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y); if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor())) { let entity = g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner); if (entity) entities.push(entity); } if (!skipFirstWall || i != 0) { let cornerLength = getWallElement(cornerWallElement, style).length / 2; let cornerAngle = angleToCorner + angleAdd / 2; let targetCorner = (i + 1) % numCorners; let cornerPosition = new Vector2D(cornerLength, 0).rotate(-cornerAngle).perpendicular(); entities = entities.concat( placeLinearWall( // Adjustment to the corner element width (approximately) Vector2D.sub(corners[i], cornerPosition), Vector2D.add(corners[targetCorner], cornerPosition), wallPart, style, playerId, undefined, constraints)); } } return entities; } /** * Places an irregular polygonal wall consisting of parts semi-randomly * chosen from a provided assortment, built around a central point at a * given radius. * * Note: Any "bending" wall pieces passed will be ... I'm not sure. TODO: test what happens! * * Note: The wallPartsAssortment is last because it's the hardest to set. * * @param {Vector2D} centerPosition * @param {number} radius * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners. * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Direction the first wallpiece or opening in the wall faces. * @param {number} [numCorners] - How many corners the polygon will have. * @param {number} [irregularity] - How irregular the polygon will be. 0 = regular, 1 = VERY irregular. * @param {boolean} [skipFirstWall] - If true, the first linear wall part will be left open as an entrance. * @param {array} [wallPartsAssortment] - An array of wall part arrays to choose from for each linear wall connecting the corners. */ function placeIrregularPolygonalWall(centerPosition, radius, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners, irregularity = 0.5, skipFirstWall = false, wallPartsAssortment = undefined, constraints = undefined) { style = validateStyle(style, playerId); numCorners = numCorners || randIntInclusive(5, 7); // Generating a generic wall part assortment with each wall part including 1 gate lengthened by walls and towers // NOTE: It might be a good idea to write an own function for that... let defaultWallPartsAssortment = [["short"], ["medium"], ["long"], ["gate", "tower", "short"]]; let centeredWallPart = ["gate"]; let extendingWallPartAssortment = [["tower", "long"], ["tower", "medium"]]; defaultWallPartsAssortment.push(centeredWallPart); for (let assortment of extendingWallPartAssortment) { let wallPart = centeredWallPart; for (let j = 0; j < radius; ++j) { if (j % 2 == 0) wallPart = wallPart.concat(assortment); else { assortment.reverse(); wallPart = assortment.concat(wallPart); assortment.reverse(); } defaultWallPartsAssortment.push(wallPart); } } // Setup optional arguments to the default wallPartsAssortment = wallPartsAssortment || defaultWallPartsAssortment; // Setup angles let angleToCover = Math.PI * 2; let angleAddList = []; for (let i = 0; i < numCorners; ++i) { // Randomize covered angles. Variety scales down with raising angle though... angleAddList.push(angleToCover / (numCorners - i) * (1 + randFloat(-irregularity, irregularity))); angleToCover -= angleAddList[angleAddList.length - 1]; } // Setup corners let corners = []; let angleActual = orientation - angleAddList[0] / 2; for (let i = 0; i < numCorners; ++i) { corners.push(Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleActual))); if (i < numCorners - 1) angleActual += angleAddList[i + 1]; } // Setup best wall parts for the different walls (a bit confusing naming...) let wallPartLengths = []; let maxWallPartLength = 0; for (let wallPart of wallPartsAssortment) { let length = getWallLength(style, wallPart); wallPartLengths.push(length); if (length > maxWallPartLength) maxWallPartLength = length; } let wallPartList = []; // This is the list of the wall parts to use for the walls between the corners, not to confuse with wallPartsAssortment! for (let i = 0; i < numCorners; ++i) { let bestWallPart = []; // This is a simple wall part not a wallPartsAssortment! let bestWallLength = Infinity; let targetCorner = (i + 1) % numCorners; // NOTE: This is not quite the length the wall will be in the end. Has to be tweaked... let wallLength = corners[i].distanceTo(corners[targetCorner]); let numWallParts = Math.ceil(wallLength / maxWallPartLength); for (let partIndex = 0; partIndex < wallPartsAssortment.length; ++partIndex) { let linearWallLength = numWallParts * wallPartLengths[partIndex]; if (linearWallLength < bestWallLength && linearWallLength > wallLength) { bestWallPart = wallPartsAssortment[partIndex]; bestWallLength = linearWallLength; } } wallPartList.push(bestWallPart); } // Place Corners and walls let entities = []; let constraint = new StaticConstraint(constraints); for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y); if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor())) entities.push( g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner)); if (!skipFirstWall || i != 0) { let cornerLength = getWallElement(cornerWallElement, style).length / 2; let targetCorner = (i + 1) % numCorners; let startAngle = angleToCorner + angleAddList[i] / 2; let targetAngle = angleToCorner + angleAddList[targetCorner] / 2; entities = entities.concat( placeLinearWall( // Adjustment to the corner element width (approximately) Vector2D.sub(corners[i], new Vector2D(cornerLength, 0).perpendicular().rotate(-startAngle)), Vector2D.add(corners[targetCorner], new Vector2D(cornerLength, 0).rotate(-targetAngle - Math.PI / 2)), wallPartList[i], style, playerId, false, constraints)); } } return entities; } /** * Places a generic fortress with towers at the edges connected with long * walls and gates, positioned around a central point at a given radius. * * The difference between this and the other two Fortress placement functions * is that those place a predefined fortress, regardless of terrain type. * This function attempts to intelligently place a wall circuit around * the central point taking into account terrain and other obstacles. * * This is the default Iberian civ bonus starting wall. * * @param {Vector2D} center - The approximate center coordinates of the fortress * @param {number} [radius] - The approximate radius of the wall to be placed. * @param {number} [playerId] * @param {string} [style] * @param {number} [irregularity] - 0 = circle, 1 = very spiky * @param {number} [gateOccurence] - Integer number, every n-th walls will be a gate instead. * @param {number} [maxTries] - How often the function tries to find a better fitting shape. */ function placeGenericFortress(center, radius = 20, playerId = 0, style, irregularity = 0.5, gateOccurence = 3, maxTries = 100, constraints = undefined) { style = validateStyle(style, playerId); // Setup some vars let startAngle = randomAngle(); let actualOff = new Vector2D(radius, 0).rotate(-startAngle); let actualAngle = startAngle; let pointDistance = getWallLength(style, ["long", "tower"]); // Searching for a well fitting point derivation let tries = 0; let bestPointDerivation; let minOverlap = 1000; let overlap; while (tries < maxTries && minOverlap > g_WallStyles[style].overlap) { let pointDerivation = []; let distanceToTarget = 1000; while (true) { let indent = randFloat(-irregularity * pointDistance, irregularity * pointDistance); let tmp = new Vector2D(radius + indent, 0).rotate(-actualAngle - pointDistance / radius); let tmpAngle = getAngle(actualOff.x, actualOff.y, tmp.x, tmp.y); actualOff.add(new Vector2D(pointDistance, 0).rotate(-tmpAngle)); actualAngle = getAngle(0, 0, actualOff.x, actualOff.y); pointDerivation.push(actualOff.clone()); distanceToTarget = pointDerivation[0].distanceTo(actualOff); let numPoints = pointDerivation.length; if (numPoints > 3 && distanceToTarget < pointDistance) // Could be done better... { overlap = pointDistance - pointDerivation[numPoints - 1].distanceTo(pointDerivation[0]); if (overlap < minOverlap) { minOverlap = overlap; bestPointDerivation = pointDerivation; } break; } } ++tries; } log("placeGenericFortress: Reduced overlap to " + minOverlap + " after " + tries + " tries"); // Place wall let entities = []; let constraint = new StaticConstraint(constraints); for (let pointIndex = 0; pointIndex < bestPointDerivation.length; ++pointIndex) { let start = Vector2D.add(center, bestPointDerivation[pointIndex]); let target = Vector2D.add(center, bestPointDerivation[(pointIndex + 1) % bestPointDerivation.length]); let angle = getAngle(start.x, start.y, target.x, target.y); let element = (pointIndex + 1) % gateOccurence == 0 ? "gate" : "long"; element = getWallElement(element, style); if (element.templateName) { let pos = Vector2D.add(start, new Vector2D(start.distanceTo(target) / 2, 0).rotate(-angle)); if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor())) entities.push(g_Map.placeEntityPassable(element.templateName, playerId, pos, angle - Math.PI / 2 + element.angle)); } // Place tower start = Vector2D.add(center, bestPointDerivation[(pointIndex + bestPointDerivation.length - 1) % bestPointDerivation.length]); angle = getAngle(start.x, start.y, target.x, target.y); let tower = getWallElement("tower", style); let pos = Vector2D.add(center, bestPointDerivation[pointIndex]); if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor())) entities.push( g_Map.placeEntityPassable(tower.templateName, playerId, pos, angle - Math.PI / 2 + tower.angle)); } return entities; } Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 27041) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 27042) @@ -1,2140 +1,2140 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronized for the biggest part, // so most of the attributes shouldn't be serialized. // Return an object with a small selection of deterministic data. return { "timeNotifications": this.timeNotifications, "timeNotificationID": this.timeNotificationID }; }; GuiInterface.prototype.Deserialize = function(data) { this.Init(); this.timeNotifications = data.timeNotifications; this.timeNotificationID = data.timeNotificationID; }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; this.templateModified = {}; this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function() { let ret = { "players": [] }; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { const playerEnt = cmpPlayerManager.GetPlayerByID(i); const cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); const cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); const cmpIdentity = Engine.QueryInterface(playerEnt, IID_Identity); // Work out which phase we are in. let phase = ""; const cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } let allies = []; let mutualAllies = []; let neutrals = []; let enemies = []; for (let j = 0; j < numPlayers; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } ret.players.push({ "name": cmpIdentity.GetName(), "civ": cmpIdentity.GetCiv(), "color": cmpPlayer.GetColor(), "controlsAll": cmpPlayer.CanControlAllUnits(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "resourceGatherers": cmpPlayer.GetResourceGatherers(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), "hasSharedLos": cmpPlayer.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": cmpPlayer.CanBarter(), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer) }); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = cmpTerrain.GetMapSize(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function() { let ret = this.GetSimulationState(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; /** * Returns the gamesettings that were chosen at the time the match started. */ GuiInterface.prototype.GetInitAttributes = function() { return InitAttributes; }; /** * This data will be stored in the replay metadata file after a match has been finished recording. */ GuiInterface.prototype.GetReplayMetadata = function() { let extendedSimState = this.GetExtendedSimulationState(); return { "timeElapsed": extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "mapSettings": InitAttributes.settings }; }; /** * Called when the game ends if the current game is part of a campaign run. */ GuiInterface.prototype.GetCampaignGameEndData = function(player) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (Trigger.prototype.OnCampaignGameEnd) return Trigger.prototype.OnCampaignGameEnd(); return {}; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function() { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({ "entity": entity, "newentity": mirage }); }; /** * Get common entity info, often used in the gui. */ GuiInterface.prototype.GetEntityState = function(player, ent) { if (!ent) return null; // All units must have a template; if not then it's a nonexistent entity id. const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(ent); if (!template) return null; const ret = { "id": ent, "player": INVALID_PLAYER, "template": template }; const cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras) ret.auras = cmpAuras.GetDescriptions(); let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) ret.identity = { "rank": cmpIdentity.GetRank(), "rankTechName": cmpIdentity.GetRankTechName(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "controllable": cmpIdentity.IsControllable() }; const cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) ret.formation = { "members": cmpFormation.GetMembers() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) ret.position = cmpPosition.GetPosition(); let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured(); ret.needsHeal = !cmpHealth.IsUnhealable(); } let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; let cmpMarket = QueryMiragedInterface(ent, IID_Market); if (cmpMarket) ret.market = { "land": cmpMarket.HasType("land"), "naval": cmpMarket.HasType("naval") }; let cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress() }; let cmpPopulation = Engine.QueryInterface(ent, IID_Population); if (cmpPopulation) ret.population = { "bonus": cmpPopulation.GetPopBonus() }; let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades": cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo(), "isUpgrading": cmpUpgrade.IsUpgrading() }; const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher); if (cmpResearcher) ret.researcher = { "technologies": cmpResearcher.GetTechnologiesList(), "techCostMultiplier": cmpResearcher.GetTechCostMultiplier() }; let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver); if (cmpStatusEffects) ret.statusEffects = cmpStatusEffects.GetActiveStatuses(); let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "queue": cmpProductionQueue.GetQueue(), "autoqueue": cmpProductionQueue.IsAutoQueueing() }; const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer); if (cmpTrainer) ret.trainer = { "entities": cmpTrainer.GetEntitiesList() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) ret.foundation = { "numBuilders": cmpFoundation.GetNumBuilders(), "buildTime": cmpFoundation.GetBuildTime() }; let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders(), "buildTime": cmpRepairable.GetBuildTime() }; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) ret.player = cmpOwnership.GetOwner(); let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "occupiedSlots": cmpGarrisonHolder.OccupiedSlots() }; let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); if (cmpTurretHolder) ret.turretHolder = { "turretPoints": cmpTurretHolder.GetTurretPoints() }; let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (cmpTurretable) ret.turretable = { "ejectable": cmpTurretable.IsEjectable(), "holder": cmpTurretable.HolderID() }; let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) ret.garrisonable = { "holder": cmpGarrisonable.HolderID(), "size": cmpGarrisonable.UnitSize() }; let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "selectableStances": cmpUnitAI.GetSelectableStances(), "isIdle": cmpUnitAI.IsIdle(), "formations": cmpUnitAI.GetFormationsList(), "formation": cmpUnitAI.GetFormationController() }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) ret.guard = { "entities": cmpGuard.GetEntities() }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } let cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) ret.gate = { "locked": cmpGate.IsLocked() }; let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) ret.alertRaiser = true; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); let cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (let type of types) { ret.attack[type] = {}; Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); ret.attack[type].attackName = cmpAttack.GetAttackName(type); ret.attack[type].splash = cmpAttack.GetSplashData(type); if (ret.attack[type].splash) Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true)); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; ret.attack[type].yOrigin = cmpAttack.GetAttackYOrigin(type); let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } if (cmpPosition && cmpPosition.IsInWorld()) // For units, take the range in front of it, no spread, so angle = 0, // else, take the average elevation around it: angle = 2 * pi. ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, ret.attack[type].yOrigin, cmpUnitAI ? 0 : 2 * Math.PI); else // Not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } let cmpResistance = QueryMiragedInterface(ent, IID_Resistance); if (cmpResistance) ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity"); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "numGatherers": cmpResourceSupply.GetNumGatherers() }; let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes(), "sharable": cmpResourceDropsite.IsSharable(), "shared": cmpResourceDropsite.IsShared() }; let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "health": cmpHeal.GetHealth(), "range": cmpHeal.GetRange().max, "interval": cmpHeal.GetInterval(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses() }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { ret.loot = cmpLoot.GetResources(); ret.loot.xp = cmpLoot.GetXp(); } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) ret.resourceTrickle = { "interval": cmpResourceTrickle.GetInterval(), "rates": cmpResourceTrickle.GetRates() }; let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure); if (cmpTreasure) ret.treasure = { "collectTime": cmpTreasure.CollectionTime(), "resources": cmpTreasure.Resources() }; let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector); if (cmpTreasureCollector) ret.treasureCollector = true; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(), "acceleration": cmpUnitMotion.GetAcceleration() }; let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep); if (cmpUpkeep) ret.upkeep = { "interval": cmpUpkeep.GetInterval(), "rates": cmpUpkeep.GetRates() }; return ret; }; GuiInterface.prototype.GetMultipleEntityStates = function(player, ents) { return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) })); }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); let rot = { "x": 0, "y": 0, "z": 0 }; let pos = { "x": cmd.x, "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z), "z": cmd.z }; const yOrigin = cmd.yOrigin || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, yOrigin, 2 * Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, data) { let templateName = data.templateName; let owner = data.player !== undefined ? data.player : player; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) - return GetTemplateDataHelper(template, owner, aurasTemplate); + return GetTemplateDataHelper(template, owner, aurasTemplate, Resources); let auraNames = template.Auras._string.split(/\s+/); for (let name of auraNames) { let auraTemplate = AuraTemplates.Get(name); if (!auraTemplate) error("Template " + templateName + " has undefined aura " + name); else aurasTemplate[name] = auraTemplate; } - return GetTemplateDataHelper(template, owner, aurasTemplate); + return GetTemplateDataHelper(template, owner, aurasTemplate, Resources); }; GuiInterface.prototype.IsTechnologyResearched = function(player, data) { if (!data.tech) return true; let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(data.tech); }; /** * Checks whether the requirements for this technology have been met. */ GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) { let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(data.tech); }; /** * Returns technologies that are being actively researched, along with * which entity is researching them and how far along the research is. */ GuiInterface.prototype.GetStartedResearch = function(player) { return QueryPlayerIDInterface(player, IID_TechnologyManager)?.GetBasicInfoOfStartedTechs() || {}; }; /** * Returns the battle state of the player. */ GuiInterface.prototype.GetBattleState = function(player) { let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (!cmpBattleDetection) return false; return cmpBattleDetection.GetState(); }; /** * Returns a list of ongoing attacks against the player. */ GuiInterface.prototype.GetIncomingAttacks = function(player) { let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); if (!cmpAttackDetection) return []; return cmpAttackDetection.GetIncomingAttacks(); }; /** * Used to show a red square over GUI elements you can't yet afford. */ GuiInterface.prototype.GetNeededResources = function(player, data) { let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player); return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {}; }; /** * State of the templateData (player dependent): true when some template values have been modified * and need to be reloaded by the gui. */ GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) { return this.templateModified[player] || false; }; GuiInterface.prototype.ResetTemplateModified = function() { this.templateModified = {}; }; /** * Some changes may require an update to the selection panel, * which is cached for efficiency. Inform the GUI it needs reloading. */ GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.SetSelectionDirty = function(player) { this.selectionDirty[player] = true; }; GuiInterface.prototype.IsSelectionDirty = function(player) { return this.selectionDirty[player] || false; }; GuiInterface.prototype.ResetSelectionDirty = function() { this.selectionDirty = {}; }; /** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) * so they should allways be added and deleted in a deterministic way. */ GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); notification.endTime = duration + cmpTimer.GetTime(); notification.id = ++this.timeNotificationID; // Let all players and observers receive the notification by default. if (!notification.players) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } this.timeNotifications.push(notification); this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime); cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID); }; GuiInterface.prototype.GetTimeNotifications = function(player) { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // Filter on players and time, since the delete timer might be executed with a delay. return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time); }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") this.AddTimeNotification(notification); else this.notifications.push(notification); }; GuiInterface.prototype.GetNotifications = function() { let n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { let cmpPlayer = QueryPlayerIDInterface(wantedPlayer); if (!cmpPlayer) return []; return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return {}; return { "name": template.Identity.GenericName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Identity.Icon }; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { return data.ents.some(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate; }); }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance) return true; } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; for (let ent of cmd.entities) { let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (let building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data) { let updateEntityColor = (iids, entities) => { for (let ent of entities) for (let iid of iids) { let cmp = Engine.QueryInterface(ent, iid); if (cmp) cmp.UpdateColor(); } }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i, IID_Player); if (!cmpPlayer) continue; cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors); if (data.displayDiplomacyColors) cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]); updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ? [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer], cmpRangeManager.GetEntitiesByPlayer(i)); } updateEntityColor([IID_Selectable, IID_StatusBars], data.selected); Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors(); }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { // Cache of owner -> color map let playerColors = {}; for (let ent of cmd.entities) { let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color. let owner = INVALID_PLAYER; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); let color = playerColors[owner]; if (!color) { color = { "r": 1, "g": 1, "b": 1 }; let cmpPlayer = QueryPlayerIDInterface(owner); if (cmpPlayer) color = cmpPlayer.GetDisplayedColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(this.entsWithAuraAndStatusBars); }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { let affectedEnts = new Set(); for (let ent of cmd.entities) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (!cmpStatusBars) continue; cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; for (let name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; for (let e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); else this.entsWithAuraAndStatusBars.delete(ent); } } for (let ent of affectedEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RegenerateSprites(); } }; GuiInterface.prototype.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player); }; GuiInterface.prototype.GetNonGaiaEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them. for (let ent of this.entsRallyPointsDisplayed) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities. for (let ent of cmd.entities) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // Entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location). let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner. let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position. let pos; if (cmd.x && cmd.z) pos = cmd; else // May return undefined if no rally point is set. pos = cmpRallyPoint.GetPositions()[0]; if (pos) { // Only update the position if we changed it (cmd.queued is set). // Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z. if ("queued" in cmd) { if (cmd.queued == true) cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z)); else cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z)); } else if (!cmpRallyPointRenderer.IsSet()) // Rebuild the renderer when not set (when reading saved game or in case of building update). for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z)); cmpRallyPointRenderer.SetDisplayed(true); // Remember which entities have their rally points displayed so we can hide them again. this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(cmd.owner); let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": localisation info (optional) * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { let result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [] }; if (!this.placementEntity || this.placementEntity[0] != cmd.template) { if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; let pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // Set it to a red shade if this is an invalid location. let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { let wallSet = cmd.wallSet; // Did the start position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let start = { "pos": cmd.start, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // Did the end position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let end = { "pos": cmd.end, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // -------------------------------------------------------------------------------- // Do some entity cache management and check for snapping. if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // We're clearing the preview, clear the entity cache and bail. for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // Keep template data around. } return false; } for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) { let pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before. for (let type in wallSet.templates) { if (type == "curves") continue; let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, { "templateName": tpl }), }; if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } // Prevent division by zero errors further on if the start and end positions are the same. if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { // Value of 0.5 was determined through trial and error. let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; let startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { let endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // Clear the single-building preview entity (we'll be rolling our own). this.SetBuildingPlacementPreview(player, { "template": "" }); // -------------------------------------------------------------------------------- // Calculate wall placement and position preview entities. let result = { "pieces": [], "cost": { "population": 0, "time": 0 } }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) // See helpers/Walls.js. previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group. let startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true // Preview only, must not appear in the result. }); } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle }); } if (end.pos) { // Analogous to the starting side case above. if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || []; previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup()); } // If we're snapping to a foundation, add an extra preview tower and also set it to the same control group. let endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true }); } } else previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle }); } let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. let allPiecesValid = true; // Number of entities that are required to build the entire wall, regardless of validity. let numRequiredPieces = 0; for (let i = 0; i < previewEntities.length; ++i) { let entInfo = previewEntities[i]; let ent = null; let tpl = entInfo.template; let tplData = this.placementWallEntities[tpl].templateData; let entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else ent = entPool.entities[entPool.numUsed]; if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // Move piece to right location. // TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities. let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces. if (tpl === wallSet.templates.tower) { let terrainGroundPrev = null; let terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { let targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. let primaryControlGroup = ent; let secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); let validPlacement = false; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region. // TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta. let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden"; if (visible) { let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement. validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success; // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) ++numRequiredPieces; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: We should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. for (let res of Resources.GetCodes().concat(["population", "time"])) result.cost[res] += tplData.cost[res]; } let canAfford = true; let cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) canAfford = false; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } ++entPool.numUsed; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest. // (TODO: Break unlikely ties by choosing the lowest entity ID.) let minDist2 = -1; let minDistEntitySnapData = null; let radius2 = data.snapRadius * data.snapRadius; for (let ent of data.snapEntities) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = { "x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent }; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (data.snapToEdges) { let position = this.obstructionSnap.getPosition(data, template); if (position) return position; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySoundForPlayer = function(player, data) { let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player); let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(data.name); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player); }; GuiInterface.prototype.PlaySound = function(player, data) { if (!data.entity) return; PlaySound(data.name, data.entity); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. let bucket = filtered.bucket; if (bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle()) return { "idle": false }; let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned()) return { "idle": false }; const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable); if (cmpTurretable && cmpTurretable.IsTurreted()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if (!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market); return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; let firstMarket = cmpEntityTrader.GetFirstMarket(); let secondMarket = cmpEntityTrader.GetSecondMarket(); let result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGoods().amount; } else if (data.target === secondMarket) result = { "type": "is second", "gain": cmpEntityTrader.GetGoods().amount, }; else if (!firstMarket) result = { "type": "set first" }; else if (!secondMarket) result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; else result = { "type": "set first" }; return result; }; GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { return Engine.QueryInterface(data.entity, IID_Trainer)?.GetBatchTime(data.batchSize) || 0; }; GuiInterface.prototype.IsMapRevealed = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for (let ent of data.entities) { let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of traders) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { let holder = cmpUnitAI.order.data.target; let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return []; return cmpPlayer.GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; /** * List the GuiInterface functions that can be safely called by GUI scripts. * (GUI scripts are non-deterministic and untrusted, so these functions must be * appropriately careful. They are called with a first argument "player", which is * trusted and indicates the player associated with the current client; no data should * be returned unless this player is meant to be able to see it.) */ let exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetInitAttributes": 1, "GetReplayMetadata": 1, "GetCampaignGameEndData": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetMultipleEntityStates": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNotifications": 1, "GetTimeNotifications": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "UpdateDisplayedPlayerColors": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "PlaySoundForPlayer": 1, "FindIdleUnits": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "EnableVisualRangeOverlayType": 1, "SetRangeOverlays": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, "IsTemplateModified": 1, "ResetTemplateModified": 1, "IsSelectionDirty": 1, "ResetSelectionDirty": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); throw new Error("Invalid GuiInterface Call name \"" + name + "\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27041) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27042) @@ -1,170 +1,170 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Resources = { "BuildSchema": type => { let schema = ""; for (let res of ["food", "metal", "stone", "wood"]) schema += "" + "" + "" + "" + ""; return "" + schema + ""; } }; Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); // Provides `IID_ModifiersManager`, used below. Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below. // What we're testing: Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("Upgrade.js"); // Input (bare minimum needed for tests): let techs = { "alter_tower_upgrade_cost": { "modifications": [ { "value": "Upgrade/Cost/stone", "add": 60.0 }, { "value": "Upgrade/Cost/wood", "multiply": 0.5 }, { "value": "Upgrade/Time", "replace": 90 } ], "affects": ["Tower"] } }; let template = { "Identity": { "Classes": { '@datatype': "tokens", "_string": "Tower" }, "VisibleClasses": { '@datatype': "tokens", "_string": "" } }, "Upgrade": { "Tower": { "Cost": { "stone": "100", "wood": "50" }, "Entity": "structures/{civ}/defense_tower", "Time": "100" } } }; let civCode = "pony"; let playerID = 1; // Usually, the tech modifications would be worked out by the TechnologyManager // with assistance from globalscripts. This test is not about testing the // TechnologyManager, so the modifications (both with and without the technology // researched) are worked out before hand and placed here. let isResearched = false; let templateTechModifications = { "without": {}, "with": { "Upgrade/Cost/stone": [{ "affects": [["Tower"]], "add": 60 }], "Upgrade/Cost/wood": [{ "affects": [["Tower"]], "multiply": 0.5 }], "Upgrade/Time": [{ "affects": [["Tower"]], "replace": 90 }] } }; let entityTechModifications = { "without": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 100 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 50 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 100 } } }, "with": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 160 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 25 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 90 } } } }; /** * Initialise various bits. */ // System Entities: AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": pID => 10 // Called in helpers/player.js::QueryPlayerIDInterface(), as part of Tests T2 and T5. }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetTemplate": () => template, // Called in components/Upgrade.js::ChangeUpgradedEntityCount(). "TemplateExists": (templ) => true }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": () => 1, // Called in components/Upgrade.js::Upgrade(). "CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade(). }); AddMock(SYSTEM_ENTITY, IID_ModifiersManager, { "ApplyTemplateModifiers": (valueName, curValue, template, player) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate() // as part of Tests T2 and T5 below. let mods = isResearched ? templateTechModifications.with : templateTechModifications.without; if (mods[valueName]) return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue); return curValue; }, "ApplyModifiers": (valueName, curValue, ent) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToEntity() // as part of Tests T3, T6 and T7 below. let mods = isResearched ? entityTechModifications.with : entityTechModifications.without; return mods[valueName][ent].newValue; } }); // Init Player: AddMock(10, IID_Player, { "AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade(). "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). }); AddMock(10, IID_Identity, { "GetCiv": () => civCode }); // Create an entity with an Upgrade component: AddMock(20, IID_Ownership, { "GetOwner": () => playerID // Called in helpers/Player.js::QueryOwnerInterface(). }); AddMock(20, IID_Identity, { "GetCiv": () => civCode // Called in components/Upgrade.js::init(). }); AddMock(20, IID_ProductionQueue, { "HasQueuedProduction": () => false }); let cmpUpgrade = ConstructComponent(20, "Upgrade", template.Upgrade); cmpUpgrade.owner = playerID; cmpUpgrade.OnOwnershipChanged({ "to": playerID }); /** * Now to start the test proper * To start with, no techs are researched... */ // T1: Check the cost of the upgrade without a player value being passed (as it would be in the structree). -let parsed_template = GetTemplateDataHelper(template, null, {}); +let parsed_template = GetTemplateDataHelper(template, null, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T2: Check the value, with a player ID (as it would be in-session). -parsed_template = GetTemplateDataHelper(template, playerID, {}); +parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T3: Check that the value is correct within the Update Component. TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 100 }); /** * Tell the Upgrade component to start the Upgrade, * then mark the technology that alters the upgrade cost as researched. */ cmpUpgrade.Upgrade("structures/" + civCode + "/defense_tower"); isResearched = true; // T4: Check that the player-less value hasn't increased... -parsed_template = GetTemplateDataHelper(template, null, {}); +parsed_template = GetTemplateDataHelper(template, null, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T5: ...but the player-backed value has. -parsed_template = GetTemplateDataHelper(template, playerID, {}); +parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 160, "wood": 25, "time": 90 }); // T6: The upgrade component should still be using the old resource cost (but new time cost) for the upgrade in progress... TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 90 }); // T7: ...but with the upgrade cancelled, it now uses the modified value. cmpUpgrade.CancelUpgrade(playerID); TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 160, "wood": 25, "time": 90 });