Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/structures/library.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/structures/library.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/structures/library.json (revision 26000) @@ -1,13 +1,13 @@ { "type": "global", "affects": ["Structure"], "modifications": [ - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.85 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.85 } ], "auraDescription": "Structures −15% technology resource costs and research time.", "auraName": "Centre of Scholarship" } Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/maur_player_teambonus.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/maur_player_teambonus.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/maur_player_teambonus.json (revision 26000) @@ -1,17 +1,17 @@ { "type": "global", "affects": ["Temple"], "affectedPlayers": ["MutualAlly"], "modifications": [ { "value": "Cost/BuildTime", "multiply": 0.5 }, { "value": "Cost/Resources/wood", "multiply": 0.5 }, { "value": "Cost/Resources/stone", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 } ], "auraName": "Ashoka's Religious Support", "auraDescription": "Temples −50% resource costs and building time; Temple technologies −50% resource costs and research time." } Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 26000) @@ -1,626 +1,626 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ "Code", "Culture", "Name", "Emblem", "History", "Music", "CivBonuses", "StartEntities", "Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); if (!selectableOnly || data.SelectableInGameSetup) civData[data.Code] = data; } return civData; } /** * @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. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {Object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {Object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers && 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} 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 = {}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.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"), "elevationBonus": getAttackStat("ElevationBonus") }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].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.ProductionQueue) + if (template.Researcher) { ret.techCostMultiplier = {}; - for (let res in template.ProductionQueue.TechCostMultiplier) - ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); + for (const res in template.Researcher.TechCostMultiplier) + ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res); } 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. */ 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/TemplateLister.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js (revision 26000) @@ -1,142 +1,142 @@ /** * This class compiles and stores lists of which templates can be built/trained/researched by other templates. */ class TemplateLister { constructor(TemplateLoader) { this.TemplateLoader = TemplateLoader; this.templateLists = new Map(); } /** * Compile lists of templates buildable/trainable/researchable of a given civ. * * @param {Object} civCode * @param {Object} civData - Data defining every civ in the game. */ compileTemplateLists(civCode, civData) { if (this.hasTemplateLists(civCode)) return this.templateLists.get(civCode); let templatesToParse = civData[civCode].StartEntities.map(entity => entity.Template); let templateLists = { "units": new Map(), "structures": new Map(), "techs": new Map(), "wallsetPieces": new Map() }; do { const templatesThisIteration = templatesToParse; templatesToParse = []; for (let templateBeingParsed of templatesThisIteration) { let baseOfTemplateBeingParsed = this.TemplateLoader.getVariantBaseAndType(templateBeingParsed, civCode)[0]; let list = this.deriveTemplateListsFromTemplate(templateBeingParsed, civCode); for (let type in list) for (let templateName of list[type]) { if (type != "techs") { let templateVariance = this.TemplateLoader.getVariantBaseAndType(templateName, civCode); if (templateVariance[1].passthru) templateName = templateVariance[0]; } if (!templateLists[type].has(templateName)) { templateLists[type].set(templateName, [baseOfTemplateBeingParsed]); if (type != "techs") templatesToParse.push(templateName); } else if (templateLists[type].get(templateName).indexOf(baseOfTemplateBeingParsed) == -1) templateLists[type].get(templateName).push(baseOfTemplateBeingParsed); } } } while (templatesToParse.length); // Expand/filter tech pairs for (let [techCode, researcherList] of templateLists.techs) { if (!this.TemplateLoader.isPairTech(techCode)) continue; for (let subTech of this.TemplateLoader.loadTechnologyPairTemplate(techCode, civCode).techs) if (!templateLists.techs.has(subTech)) templateLists.techs.set(subTech, researcherList); else for (let researcher of researcherList) if (templateLists.techs.get(subTech).indexOf(researcher) == -1) templateLists.techs.get(subTech).push(researcher); templateLists.techs.delete(techCode); } // Remove wallset pieces, as they've served their purpose. delete templateLists.wallsetPieces; this.templateLists.set(civCode, templateLists); return this.templateLists.get(civCode); } /** * Returns a civ's template list. * * Note: this civ must have gone through the compilation process above! * * @param {string} civCode * @return {Object} containing lists of template names, grouped by type. */ getTemplateLists(civCode) { if (this.hasTemplateLists(civCode)) return this.templateLists.get(civCode); error("Template lists of \"" + civCode + "\" requested, but this civ has not been loaded."); return {}; } /** * Returns whether the civ of the given civCode has been loaded into cache. * * @param {string} civCode * @return {boolean} */ hasTemplateLists(civCode) { return this.templateLists.has(civCode); } /** * Compiles lists of buildable, trainable, or researchable entities from * a named template. */ deriveTemplateListsFromTemplate(templateName, civCode) { if (!templateName || !Engine.TemplateExists(templateName)) return {}; let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); - let templateLists = this.TemplateLoader.deriveProductionQueue(template, civCode); + const templateLists = this.TemplateLoader.deriveProduction(template, civCode); templateLists.structures = this.TemplateLoader.deriveBuildQueue(template, civCode); if (template.WallSet) { templateLists.wallsetPieces = []; for (let segment in template.WallSet.Templates) { segment = template.WallSet.Templates[segment].replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(segment)) templateLists.wallsetPieces.push(segment); } } return templateLists; } } Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/barracks_batch_training.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/technologies/barracks_batch_training.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/barracks_batch_training.json (revision 26000) @@ -1,17 +1,17 @@ { "genericName": "Conscription", "description": "Significantly increase training speed of infantry by training them in large batches or companies.", "cost": { "food": 500 }, "requirements": { "tech": "phase_city" }, "requirementsTooltip": "Unlocked in City Phase.", "icon": "fist_spear_fire.png", "researchTime": 40, "tooltip": "Barracks −10% batch training time.", "modifications": [ - { "value": "ProductionQueue/BatchTimeModifier", "add": -0.1 } + { "value": "Trainer/BatchTimeModifier", "add": -0.1 } ], "affects": ["Barracks"], "soundComplete": "interface/alarm/alarm_upgradearmory.xml" } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 26000) @@ -1,340 +1,340 @@ /** * This class handles the loading of files. */ class TemplateLoader { constructor() { /** * Raw Data Caches. */ this.auraData = {}; this.playerData = {}; this.technologyData = {}; this.templateData = {}; /** * Partly-composed data. */ this.autoResearchTechList = this.findAllAutoResearchedTechs(); } /** * Loads raw aura template. * * Loads from local cache if available, else from file system. * * @param {string} templateName * @return {Object} Object containing raw template data. */ loadAuraTemplate(templateName) { if (!(templateName in this.auraData)) { let data = Engine.ReadJSONFile(this.AuraPath + templateName + ".json"); translateObjectKeys(data, this.AuraTranslateKeys); this.auraData[templateName] = data; } return this.auraData[templateName]; } /** * Loads raw entity template. * * Loads from local cache if data present, else from file system. * * @param {string} templateName * @param {string} civCode * @return {Object} Object containing raw template data. */ loadEntityTemplate(templateName, civCode) { if (!(templateName in this.templateData)) { // We need to clone the template because we want to perform some translations. let data = clone(Engine.GetTemplate(templateName)); translateObjectKeys(data, this.EntityTranslateKeys); if (data.Auras) for (let auraID of data.Auras._string.split(/\s+/)) this.loadAuraTemplate(auraID); if (data.Identity.Civ != this.DefaultCiv && civCode != this.DefaultCiv && data.Identity.Civ != civCode) warn("The \"" + templateName + "\" template has a defined civ of \"" + data.Identity.Civ + "\". " + "This does not match the currently selected civ \"" + civCode + "\"."); this.templateData[templateName] = data; } return this.templateData[templateName]; } /** * Loads raw player template. * * Loads from local cache if data present, else from file system. * * If a civ doesn't have their own civ-specific template, * then we return the generic template. * * @param {string} civCode * @return {Object} Object containing raw template data. */ loadPlayerTemplate(civCode) { if (!(civCode in this.playerData)) { let templateName = this.buildPlayerTemplateName(civCode); this.playerData[civCode] = Engine.GetTemplate(templateName); // No object keys need to be translated } return this.playerData[civCode]; } /** * Loads raw technology template. * * Loads from local cache if available, else from file system. * * @param {string} templateName * @return {Object} Object containing raw template data. */ loadTechnologyTemplate(templateName) { if (!(templateName in this.technologyData)) { let data = Engine.ReadJSONFile(this.TechnologyPath + templateName + ".json"); translateObjectKeys(data, this.TechnologyTranslateKeys); // Translate specificName as in GetTechnologyData() from gui/session/session.js if (typeof (data.specificName) === 'object') for (let civ in data.specificName) data.specificName[civ] = translate(data.specificName[civ]); else if (data.specificName) warn("specificName should be an object of civ->name mappings in " + templateName + ".json"); this.technologyData[templateName] = data; } return this.technologyData[templateName]; } /** * @param {string} templateName * @param {string} civCode * @return {Object} Contains a list and the requirements of the techs in the pair */ loadTechnologyPairTemplate(templateName, civCode) { let template = this.loadTechnologyTemplate(templateName); return { "techs": [template.top, template.bottom], "reqs": DeriveTechnologyRequirements(template, civCode) }; } - deriveProductionQueue(template, civCode) + deriveProduction(template, civCode) { - let production = { + const production = { "techs": [], "units": [] }; - if (!template.ProductionQueue) + if (!template.Researcher && !template.Trainer) return production; - if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string) - for (let templateName of template.ProductionQueue.Entities._string.split(" ")) + if (template.Trainer?.Entities?._string) + for (let templateName of template.Trainer.Entities._string.split(" ")) { templateName = templateName.replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(templateName)) production.units.push(templateName); } - let appendTechnology = (technologyName) => { - let technology = this.loadTechnologyTemplate(technologyName, civCode); + const appendTechnology = (technologyName) => { + const technology = this.loadTechnologyTemplate(technologyName, civCode); if (DeriveTechnologyRequirements(technology, civCode)) production.techs.push(technologyName); }; - if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string) - for (let technologyName of template.ProductionQueue.Technologies._string.split(" ")) + if (template.Researcher?.Technologies?._string) + for (let technologyName of template.Researcher.Technologies._string.split(" ")) { if (technologyName.indexOf("{civ}") != -1) { - let civTechName = technologyName.replace("{civ}", civCode); + const civTechName = technologyName.replace("{civ}", civCode); technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic"); } if (this.isPairTech(technologyName)) { let technologyPair = this.loadTechnologyPairTemplate(technologyName, civCode); if (technologyPair.reqs) for (technologyName of technologyPair.techs) appendTechnology(technologyName); } else appendTechnology(technologyName); } return production; } deriveBuildQueue(template, civCode) { let buildQueue = []; if (!template.Builder || !template.Builder.Entities._string) return buildQueue; for (let build of template.Builder.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(build)) buildQueue.push(build); } return buildQueue; } deriveModifications(civCode, auraList) { const modificationData = []; for (const techName of this.autoResearchTechList) modificationData.push(GetTechnologyBasicDataHelper(this.loadTechnologyTemplate(techName), civCode)); for (const auraName of auraList) modificationData.push(this.loadAuraTemplate(auraName)); return DeriveModificationsFromTechnologies(modificationData); } /** * If a civ doesn't have its own civ-specific player template, * this returns the name of the generic player template. * * @see simulation/helpers/Player.js GetPlayerTemplateName() * (Which can't be combined with this due to different Engine contexts) */ buildPlayerTemplateName(civCode) { let templateName = this.PlayerPath + this.PlayerTemplatePrefix + civCode; if (Engine.TemplateExists(templateName)) return templateName; return this.PlayerPath + this.PlayerTemplateFallback; } /** * Crudely iterates through every tech JSON file and identifies those * that are auto-researched. * * @return {array} List of techs that are researched automatically */ findAllAutoResearchedTechs() { let techList = []; for (let templateName of listFiles(this.TechnologyPath, ".json", true)) { let data = this.loadTechnologyTemplate(templateName); if (data && data.autoResearch) techList.push(templateName); } return techList; } /** * A template may be a variant of another template, * eg. `*_house`, `*_trireme`, or a promotion. * * This method returns an array containing: * [0] - The template's basename * [1] - The variant type * [2] - Further information (if available) * * e.g.: * units/athen/infantry_swordsman_e * -> ["units/athen/infantry_swordsman_b", TemplateVariant.promotion, "elite"] * * units/brit/support_female_citizen_house * -> ["units/brit/support_female_citizen", TemplateVariant.unlockedByTechnology, "unlock_female_house"] */ getVariantBaseAndType(templateName, civCode) { if (!templateName || !Engine.TemplateExists(templateName)) return undefined; templateName = removeFiltersFromTemplateName(templateName); let template = this.loadEntityTemplate(templateName, civCode); if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName)) return [templateName, TemplateVariant.base]; let parentTemplate = this.loadEntityTemplate(template["@parent"], civCode); let inheritedVariance = this.getVariantBaseAndType(template["@parent"], civCode); if (parentTemplate.Identity) { if (parentTemplate.Identity.Civ && parentTemplate.Identity.Civ != template.Identity.Civ) return [templateName, TemplateVariant.base]; if (parentTemplate.Identity.Rank && parentTemplate.Identity.Rank != template.Identity.Rank) return [inheritedVariance[0], TemplateVariant.promotion, template.Identity.Rank.toLowerCase()]; } if (parentTemplate.Upgrade) for (let upgrade in parentTemplate.Upgrade) if (parentTemplate.Upgrade[upgrade].Entity) return [inheritedVariance[0], TemplateVariant.upgrade, upgrade.toLowerCase()]; if (template.Identity.RequiredTechnology) return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.RequiredTechnology]; if (parentTemplate.Cost) for (let res in parentTemplate.Cost.Resources) if (+parentTemplate.Cost.Resources[res]) return [inheritedVariance[0], TemplateVariant.trainable]; warn("Template variance unknown: " + templateName); return [templateName, TemplateVariant.unknown]; } isPairTech(technologyCode) { return !!this.loadTechnologyTemplate(technologyCode).top; } isPhaseTech(technologyCode) { return basename(technologyCode).startsWith("phase"); } } /** * Paths to certain files. * * It might be nice if we could get these from somewhere, instead of having them hardcoded here. */ TemplateLoader.prototype.AuraPath = "simulation/data/auras/"; TemplateLoader.prototype.PlayerPath = "special/player/"; TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/"; TemplateLoader.prototype.DefaultCiv = "gaia"; /** * Expected prefix for player templates, and the file to use if a civ doesn't have its own. */ TemplateLoader.prototype.PlayerTemplatePrefix = "player_"; TemplateLoader.prototype.PlayerTemplateFallback = "player"; /** * Keys of template values that are to be translated on load. */ TemplateLoader.prototype.AuraTranslateKeys = ["auraName", "auraDescription"]; TemplateLoader.prototype.EntityTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"]; TemplateLoader.prototype.TechnologyTranslateKeys = ["genericName", "tooltip", "description"]; Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/ptol_catafalque.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/ptol_catafalque.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/ptol_catafalque.json (revision 26000) @@ -1,12 +1,12 @@ { "type": "global", "affects": ["Structure"], "modifications": [ - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 } ], "auraName": "Great Librarian", "auraDescription": "Continuing his predecessors' work on the Great Library of Alexandria, he seized every book brought to the city, thus leaving to his people a vast amount of hoarded wisdom.\nStructure technologies −10% resource costs." } Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/athen_hero_themistocles_1.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/athen_hero_themistocles_1.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/athen_hero_themistocles_1.json (revision 26000) @@ -1,11 +1,11 @@ { "type": "garrison", "affects": ["Ship"], "affectedPlayers": ["MutualAlly"], "modifications": [ - { "value": "ProductionQueue/BatchTimeModifier", "multiply": 0.7 }, + { "value": "Trainer/BatchTimeModifier", "multiply": 0.7 }, { "value": "UnitMotion/WalkSpeed", "multiply": 1.5 } ], "auraName": "Naval Commander", "auraDescription": "When garrisoned, the Ship has −30% batch training time and +50% movement speed." } Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_chanakya_1.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_chanakya_1.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_chanakya_1.json (revision 26000) @@ -1,14 +1,14 @@ { "type": "garrison", "affects": ["Structure"], "modifications": [ - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.8 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.8 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.8 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.8 }, - { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.8 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.8 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.8 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.8 }, + { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 } ], "auraDescription": "When garrisoned, the Structure's technologies have −20% resource cost and −50% research time.", "auraName": "Teacher", "overlayIcon": "art/textures/ui/session/auras/build_bonus.png" } Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/stable_batch_training.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/technologies/stable_batch_training.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/stable_batch_training.json (revision 26000) @@ -1,17 +1,17 @@ { "genericName": "Conscription", "description": "Significantly increase training speed of cavalry by training them in large batches or squadrons.", "cost": { "food": 500 }, "requirements": { "tech": "phase_city" }, "requirementsTooltip": "Unlocked in City Phase.", "icon": "horseshoe_gold.png", "researchTime": 40, "tooltip": "Stables −10% batch training time.", "modifications": [ - { "value": "ProductionQueue/BatchTimeModifier", "add": -0.1 } + { "value": "Trainer/BatchTimeModifier", "add": -0.1 } ], "affects": ["Stable"], "soundComplete": "interface/alarm/alarm_upgradearmory.xml" } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 26000) @@ -1,195 +1,197 @@ function Cheat(input) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerManager || input.player < 0) return; var playerEnt = cmpPlayerManager.GetPlayerByID(input.player); if (playerEnt == INVALID_ENTITY) return; var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); if (!cmpPlayer) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (!cmpPlayer.GetCheatsEnabled()) return; switch(input.action) { case "addresource": cmpPlayer.AddResource(input.text, input.parameter); return; case "revealmap": var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, true); return; case "maxpopulation": cmpPlayer.SetPopulationBonuses((cmpPlayerManager.GetMaxWorldPopulation() || cmpPlayer.GetMaxPopulation()) + 500); return; case "changemaxpopulation": { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("cheat/maxpopulation", { "Player/MaxPopulation": [{ "affects": ["Player"], "add": 500 }], }, playerEnt); return; } case "convertunit": for (let ent of input.selected) { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(cmpPlayer.GetPlayerID()); } return; case "killunits": for (let ent of input.selected) { let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } return; case "defeatplayer": cmpPlayer = QueryPlayerIDInterface(input.parameter); if (cmpPlayer) cmpPlayer.SetState("defeated", markForTranslation("%(player)s has been defeated (cheat).")); return; case "createunits": - var cmpProductionQueue = input.selected.length && Engine.QueryInterface(input.selected[0], IID_ProductionQueue); - if (!cmpProductionQueue) + { + const cmpTrainer = input.selected.length && Engine.QueryInterface(input.selected[0], IID_Trainer); + if (!cmpTrainer) { cmpGuiInterface.PushNotification({ "type": "text", "players": [input.player], "message": markForTranslation("You need to select a building that trains units."), "translateMessage": true }); return; } let owner = input.player; - let cmpOwnership = Engine.QueryInterface(input.selected[0], IID_Ownership); + const cmpOwnership = Engine.QueryInterface(input.selected[0], IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); for (let i = 0; i < Math.min(input.parameter, cmpPlayer.GetMaxPopulation() - cmpPlayer.GetPopulationCount()); ++i) - cmpProductionQueue.SpawnUnits({ - "player": owner, - "metadata": null, - "entity": { - "template": input.templates[i % input.templates.length], - "count": 1 - } - }); + { + const batch = new cmpTrainer.Item(input.templates[i % input.templates.length], 1, input.selected[0], null); + batch.player = owner; + batch.Finish(); + // ToDo: If not able to spawn, cancel the batch. + } return; + } case "fastactions": { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); if (cmpModifiersManager.HasAnyModifier("cheat/fastactions", playerEnt)) cmpModifiersManager.RemoveAllModifiers("cheat/fastactions", playerEnt); else cmpModifiersManager.AddModifiers("cheat/fastactions", { "Cost/BuildTime": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }], "ResourceGatherer/BaseSpeed": [{ "affects": [["Structure"], ["Unit"]], "multiply": 1000 }], "Pack/Time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }], "Upgrade/Time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }], - "ProductionQueue/TechCostMultiplier/time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }] + "Researcher/TechCostMultiplier/time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }] }, playerEnt); return; } case "changephase": var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (!cmpTechnologyManager) return; // store the phase we want in the next input parameter var parameter; if (!cmpTechnologyManager.IsTechnologyResearched("phase_town")) parameter = "phase_town"; else if (!cmpTechnologyManager.IsTechnologyResearched("phase_city")) parameter = "phase_city"; else return; if (TechnologyTemplates.Has(parameter + "_" + cmpPlayer.civ)) parameter += "_" + cmpPlayer.civ; else parameter += "_generic"; Cheat({ "player": input.player, "action": "researchTechnology", "parameter": parameter, "selected": input.selected }); return; case "researchTechnology": + { if (!input.parameter.length) return; var techname = input.parameter; var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (!cmpTechnologyManager) return; // check, if building is selected if (input.selected[0]) { - var cmpProductionQueue = Engine.QueryInterface(input.selected[0], IID_ProductionQueue); - if (cmpProductionQueue) + const cmpResearcher = Engine.QueryInterface(input.selected[0], IID_Researcher); + if (cmpResearcher) { // try to spilt the input var tmp = input.parameter.split(/\s+/); var number = +tmp[0]; var pair = tmp.length > 1 && (tmp[1] == "top" || tmp[1] == "bottom") ? tmp[1] : "top"; // use top as default value // check, if valid number was parsed. if (number || number === 0) { // get name of tech - var techs = cmpProductionQueue.GetTechnologiesList(); + const techs = cmpResearcher.GetTechnologiesList(); if (number > 0 && number <= techs.length) { var tech = techs[number-1]; if (!tech) return; // get name of tech if (tech.pair) techname = tech[pair]; else techname = tech; } else return; } } } if (TechnologyTemplates.Has(techname) && !cmpTechnologyManager.IsTechnologyResearched(techname)) cmpTechnologyManager.ResearchTechnology(techname); return; + } case "metaCheat": for (let resource of Resources.GetCodes()) Cheat({ "player": input.player, "action": "addresource", "text": resource, "parameter": input.parameter }); Cheat({ "player": input.player, "action": "maxpopulation" }); Cheat({ "player": input.player, "action": "changemaxpopulation" }); Cheat({ "player": input.player, "action": "fastactions" }); for (let i=0; i<2; ++i) Cheat({ "player": input.player, "action": "changephase", "selected": input.selected }); return; case "playRetro": let play = input.parameter.toLowerCase() != "off"; cmpGuiInterface.PushNotification({ "type": "play-tracks", "tracks": play && input.parameter.split(" "), "lock": play, "players": [input.player] }); return; default: warn("Cheat '" + input.action + "' is not implemented"); return; } } Engine.RegisterGlobal("Cheat", Cheat); Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml (revision 26000) @@ -1,45 +1,45 @@ 25 50.0 5 0.5 8.0 athen Market Settlement Greek Polis This is a major Greek city. - + + true + 300 + 35000 + + -units/{civ}/support_female_citizen campaigns/army_mace_hero_alexander campaigns/army_mace_standard units/{civ}/support_trader - - - true - 300 - 35000 - + campaigns/structures/hellenes/settlement_curtainwall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/mixins/trading_post.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/mixins/trading_post.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/mixins/trading_post.xml (revision 26000) @@ -1,25 +1,25 @@ - - - - 500 - - - - Trading Post - true - - - - -trader_health - -trade_gain_01 - -trade_gain_02 - -trade_commercial_treaty - - - - - - - structures/fndn_5x5.xml - - + + + + 500 + + + + Trading Post + true + + + + -trader_health + -trade_gain_01 + -trade_gain_02 + -trade_commercial_treaty + + + + + + + structures/fndn_5x5.xml + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_civil_centre.xml (revision 26000) @@ -1,13 +1,15 @@ skirm + structures/{civ}/civil_centre + structures/athenians/civil_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml (revision 26000) @@ -1,44 +1,44 @@ 25 50.0 3 0.5 8.0 athen Market Settlement Minor Greek Polis This is a minor Greek city. - - - -units/{civ}/support_female_citizen - campaigns/army_mace_hero_alexander - campaigns/army_mace_standard - - true 150 35000 + + + -units/{civ}/support_female_citizen + campaigns/army_mace_hero_alexander + campaigns/army_mace_standard + + campaigns/structures/hellenes/settlement_curtainwall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/mixins/shrine.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/mixins/shrine.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/mixins/shrine.xml (revision 26000) @@ -1,43 +1,45 @@ - - - - own neutral ally - - - - 8.0 - - - - - Shrine - - true - - - - - - - -units/{civ}/support_healer_b - units/{native}/support_healer_e - - - -heal_range - -heal_range_2 - -heal_rate - -heal_rate_2 - -garrison_heal - -health_regen_units - - - - 15.0 - - - - - - structures/fndn_4x4.xml - - + + + + own neutral ally + + + + 8.0 + + + + + Shrine + + true + + + + + + + -heal_range + -heal_range_2 + -heal_rate + -heal_rate_2 + -garrison_heal + -health_regen_units + + + + + 15.0 + + + + + + -units/{civ}/support_healer_b + units/{native}/support_healer_e + + + + structures/fndn_4x4.xml + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_barracks.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_barracks.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_barracks.xml (revision 26000) @@ -1,13 +1,15 @@ skirm + structures/{civ}/barracks + structures/athenians/barracks.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_dock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_dock.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_dock.xml (revision 26000) @@ -1,13 +1,15 @@ skirm + structures/{civ}/dock + structures/athenians/dock.xml Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 26000) @@ -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] || {}); parsed.name.internal = templateName; parsed.history = template.Identity.History; - parsed.production = this.TemplateLoader.deriveProductionQueue(template, civCode); + 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); 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] || {}); 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/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 26000) @@ -1,1716 +1,1716 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too. const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; const ACTION_GUARD = 3; const ACTION_PATROL = 4; const ACTION_OCCUPY_TURRET = 5; const ACTION_CALLTOARMS = 6; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; const INPUT_SELECTING = 1; const INPUT_BANDBOXING = 2; const INPUT_BUILDING_PLACEMENT = 3; const INPUT_BUILDING_CLICK = 4; const INPUT_BUILDING_DRAG = 5; const INPUT_BATCHTRAINING = 6; const INPUT_PRESELECTEDACTION = 7; const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; const INPUT_UNIT_POSITION_START = 10; const INPUT_UNIT_POSITION = 11; const INPUT_FLARE = 12; var inputState = INPUT_NORMAL; const INVALID_ENTITY = 0; var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; /** * Containing the ingame position which span the line. */ var g_FreehandSelection_InputLine = []; /** * Minimum squared distance when a mouse move is called a drag. */ const g_FreehandSelection_ResolutionInputLineSquared = 1; /** * Minimum length a dragged line should have to use the freehand selection. */ const g_FreehandSelection_MinLengthOfLine = 8; /** * To start the freehandSelection function you need a minimum number of units. * Minimum must be 2, for better performance you could set it higher. */ const g_FreehandSelection_MinNumberOfUnits = 2; /** * Number of pixels the mouse can move before the action is considered a drag. */ const g_MaxDragDelta = 4; /** * Used for remembering mouse coordinates at start of drag operations. */ var g_DragStart; /** * Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. * If any mousedown or mouseup of a sequence of clicks lands on a unit, * that unit will be selected, which makes it easier to click on moving units. */ var clickedEntity = INVALID_ENTITY; /** * Store the last time the flare functionality was used to prevent overusage. */ var g_LastFlareTime; /** * The duration in ms for which we disable flaring after each flare to prevent overusage. */ const g_FlareCooldown = 3000; // Same double-click behaviour for hotkey presses. const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { let cursorSet = false; let tooltipSet = false; let informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); if (inputState == INPUT_FLARE || inputState == INPUT_NORMAL && Engine.HotkeyIsPressed("session.flare") && !g_IsObserver) { Engine.SetCursor("action-flare"); cursorSet = true; } else if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap()) { let action = determineAction(mouseX, mouseY, g_MiniMapPanel.isMouseOverMiniMap()); if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); cursorSet = true; } if (action.tooltip) { tooltipSet = true; informationTooltip.caption = action.tooltip; informationTooltip.hidden = false; } } } if (!cursorSet) Engine.ResetCursor(); if (!tooltipSet) informationTooltip.hidden = true; let placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); if (placementSupport.tooltipMessage) placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip"; placementTooltip.caption = placementSupport.tooltipMessage || ""; placementTooltip.hidden = !placementSupport.tooltipMessage; } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to. // See onSimulationUpdate in session.js. if (placementSupport.mode === "building") { if (placementSupport.template && placementSupport.position) { let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); placementSupport.tooltipError = !result.success; placementSupport.tooltipMessage = ""; if (!result.success) { if (result.message && result.parameters) { let message = result.message; if (result.translateMessage) if (result.pluralMessage) message = translatePlural(result.message, result.pluralMessage, result.pluralCount); else message = translate(message); let parameters = result.parameters; if (result.translateParameters) translateObjectKeys(parameters, result.translateParameters); placementSupport.tooltipMessage = sprintf(message, parameters); } return false; } if (placementSupport.attack && placementSupport.attack.Ranged) { let cmd = { "x": placementSupport.position.x, "z": placementSupport.position.z, "range": placementSupport.attack.Ranged.maxRange, "elevationBonus": placementSupport.attack.Ranged.elevationBonus }; let averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); let range = Math.round(cmd.range); placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); } return true; } } else if (placementSupport.mode === "wall" && placementSupport.wallSet && placementSupport.position) { placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities( placementSupport.wallSet.templates.tower, placementSupport.wallSnapEntitiesIncludeOffscreen, true, // require exact template match true // include foundations ); return Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": placementSupport.wallSet, "start": placementSupport.position, "end": placementSupport.wallEndPosition, "snapEntities": placementSupport.wallSnapEntities // snapping entities (towers) for starting a wall segment }); } return false; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMiniMap) { let selection = g_Selection.toList(); if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } let entState = GetEntityState(selection[0]); if (!entState) return undefined; if (!selection.every(ownsEntity) && !(g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll)) return undefined; let target; if (!fromMiniMap) { let ent = Engine.PickEntityAtPoint(x, y); if (ent != INVALID_ENTITY) target = ent; } // Decide between the following ordered actions, // if two actions are possible, the first one is taken // thus the most specific should appear first. if (preSelectedAction != ACTION_NONE) { for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].preSelectedActionCheck) { let r = g_UnitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].hotkeyActionCheck) { let r = g_UnitActions[action].hotkeyActionCheck(target, selection); if (r) return r; } for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].actionCheck) { let r = g_UnitActions[action].actionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } function ownsEntity(ent) { let entState = GetEntityState(ent); return entState && entState.player == g_ViewedPlayer; } function isAttackMovePressed() { return Engine.HotkeyIsPressed("session.attackmove") || Engine.HotkeyIsPressed("session.attackmoveUnit"); } function isSnapToEdgesEnabled() { let config = Engine.ConfigDB_GetValue("user", "gui.session.snaptoedges"); let hotkeyPressed = Engine.HotkeyIsPressed("session.snaptoedges"); return hotkeyPressed == (config == "disabled"); } function tryPlaceBuilding(queued, pushFront) { if (placementSupport.mode !== "building") { error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'"); return false; } if (!updateBuildingPlacementPreview()) { Engine.GuiInterfaceCall("PlaySound", { "name": "invalid_building_placement", "entity": g_Selection.getFirstSelected() }); return false; } let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList(); Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); if (!queued || !g_Selection.size()) placementSupport.Reset(); else placementSupport.RandomizeActorSeed(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'"); return false; } let wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof wallPlacementInfo === "object")) { error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo)); return false; } if (!wallPlacementInfo) return false; let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList(); let cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, "formation": g_AutoFormation.getNull() }; // Make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed // (this is somewhat non-ideal and hardcode-ish). let hasWallSegment = false; for (let piece of cmd.pieces) { if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( { hasWallSegment = true; break; } } if (hasWallSegment) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); } return true; } /** * Updates the bandbox object with new positions and visibility. * @returns {array} The coordinates of the vertices of the bandbox. */ function updateBandbox(bandbox, ev, hidden) { let scale = +Engine.ConfigDB_GetValue("user", "gui.scale"); let vMin = Vector2D.min(g_DragStart, ev); let vMax = Vector2D.max(g_DragStart, ev); bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale); bandbox.hidden = hidden; return [vMin.x, vMin.y, vMax.x, vMax.y]; } // Define some useful unit filters for getPreferredEntities. var unitFilters = { "isUnit": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit"); }, "isDefensive": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Defensive"); }, "isMilitary": entity => { let entState = GetEntityState(entity); return entState && g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isNonMilitary": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && !g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isIdle": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.unitAI && entState.unitAI.isIdle && !hasClass(entState, "Domestic"); }, "isWounded": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.maxHitpoints && 100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold"); }, "isAnything": entity => { return true; } }; // Choose, inside a list of entities, which ones will be selected. // We may use several entity filters, until one returns at least one element. function getPreferredEntities(ents) { let filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; if (Engine.HotkeyIsPressed("selection.militaryonly")) filters = [unitFilters.isMilitary]; if (Engine.HotkeyIsPressed("selection.nonmilitaryonly")) filters = [unitFilters.isNonMilitary]; if (Engine.HotkeyIsPressed("selection.idleonly")) filters = [unitFilters.isIdle]; if (Engine.HotkeyIsPressed("selection.woundedonly")) filters = [unitFilters.isWounded]; let preferredEnts = []; for (let i = 0; i < filters.length; ++i) { preferredEnts = ents.filter(filters[i]); if (preferredEnts.length) break; } return preferredEnts; } function handleInputBeforeGui(ev, hoveredObject) { if (GetSimState().cinemaPlaying) return false; // Capture cursor position so we can use it for displaying cursors, // and key states. switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } mouseIsOverObject = (hoveredObject != null); // Close the menu when interacting with the game world. if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) g_Menu.close(); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: let bandbox = Engine.GetGUIObjectByName("bandbox"); switch (ev.type) { case "mousemotion": { let rect = updateBandbox(bandbox, ev, false); let ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); let preferredEntities = getPreferredEntities(ents); g_Selection.setHighlightList(preferredEntities); return false; } case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { let rect = updateBandbox(bandbox, ev, true); let ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); g_Selection.setHighlightList([]); if (Engine.HotkeyIsPressed("selection.add")) g_Selection.addList(ents); else if (Engine.HotkeyIsPressed("selection.remove")) g_Selection.removeList(ents); else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection. bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION: switch (ev.type) { case "mousemotion": return positionUnitsFreehandSelectionMouseMove(ev); case "mousebuttonup": return positionUnitsFreehandSelectionMouseUp(ev); } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode. let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If queued, let the player continue placing another of the same building. let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront"))) { if (queued && g_Selection.size()) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else inputState = INPUT_BUILDING_PLACEMENT; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_CLICK: // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. switch (ev.type) { case "mousebuttonup": if (ev.button === SDL_BUTTON_LEFT) { inputState = INPUT_BUILDING_WALL_PATHING; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_PATHING: // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the // user to continue building walls. switch (ev.type) { case "mousemotion": placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); // Update the structure placement preview, and by extension, the list of snapping candidate entities for both (!) // the ending point and the starting point to snap to. // // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers // in them. Might be useful to query only for entities within a certain range around the starting point and ending // points. placementSupport.wallSnapEntitiesIncludeOffscreen = true; let result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost }); placementSupport.tooltipMessage = [ getEntityCostTooltip(result), getNeededResourcesTooltip(neededResources) ].filter(tip => tip).join("\n"); } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceWall(queued)) { if (queued) { // Continue building, just set a new starting position where we left off. placementSupport.position = placementSupport.wallEndPosition; placementSupport.wallEndPosition = undefined; inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.Reset(); inputState = INPUT_NORMAL; } } else placementSupport.tooltipMessage = translate("Cannot build wall here!"); updateBuildingPlacementPreview(); return true; } if (ev.button == SDL_BUTTON_RIGHT) { placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_DRAG: switch (ev.type) { case "mousemotion": let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) // Rotate in the direction of the cursor. placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); else // If the cursor is near the center, snap back to the default orientation. placementSupport.SetDefaultAngle(); let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } updateBuildingPlacementPreview(); break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If queued, let the player continue placing another of the same structure. let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront"))) { if (queued && g_Selection.size()) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else inputState = INPUT_BUILDING_PLACEMENT; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BATCHTRAINING: if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") { flushTrainingBatch(); inputState = INPUT_NORMAL; } break; } return false; } function handleInputAfterGui(ev) { if (GetSimState().cinemaPlaying) return false; if (ev.hotkey === undefined) ev.hotkey = null; if (ev.hotkey == "session.highlightguarding") { g_ShowGuarding = (ev.type == "hotkeypress"); updateAdditionalHighlight(); } else if (ev.hotkey == "session.highlightguarded") { g_ShowGuarded = (ev.type == "hotkeypress"); updateAdditionalHighlight(); } if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) clickedEntity = INVALID_ENTITY; // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": let ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (Engine.HotkeyIsPressed("session.flare") && controlsPlayer(g_ViewedPlayer)) { triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); return true; } if (ev.button == SDL_BUTTON_LEFT) { g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_SELECTING; // If a single click occured, reset the clickedEntity. // Also set it if we're double/triple clicking and missed the unit earlier. if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { if (!controlsPlayer(g_ViewedPlayer)) break; g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_UNIT_POSITION_START; } break; case "hotkeypress": if (ev.hotkey.indexOf("selection.group.") == 0) { let now = Date.now(); if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { let sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { let sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now; prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousemotion": let ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { let action = determineAction(ev.x, ev.y); if (!action) break; if (!Engine.HotkeyIsPressed("session.queue") && !Engine.HotkeyIsPressed("session.orderone")) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; } return doAction(action, ev); } if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } default: // Slight hack: If selection is empty, reset the input state. if (!g_Selection.size()) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta) { inputState = INPUT_BANDBOXING; return false; } let ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { if (clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); // Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event. if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity)) { clickedEntity = INVALID_ENTITY; if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { g_Selection.reset(); resetIdleUnit(); } inputState = INPUT_NORMAL; return true; } if (Engine.GetFollowedEntity() != clickedEntity) Engine.CameraFollow(0); let ents = []; if (ev.clicks == 1) ents = [clickedEntity]; else { let showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); let matchRank = true; let templateToMatch; if (ev.clicks == 2) { templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName; if (templateToMatch) matchRank = false; else // No selection group name defined, so fall back to exact match. templateToMatch = GetEntityState(clickedEntity).template; } else // Triple click // Select units matching exact template name (same rank). templateToMatch = GetEntityState(clickedEntity).template; // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); } if (Engine.HotkeyIsPressed("selection.add")) g_Selection.addList(ents); else if (Engine.HotkeyIsPressed("selection.remove")) g_Selection.removeList(ents); else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION_START: switch (ev.type) { case "mousemotion": if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta)) { inputState = INPUT_UNIT_POSITION; return false; } break; case "mousebuttonup": inputState = INPUT_NORMAL; if (ev.button == SDL_BUTTON_RIGHT) { let action = determineAction(ev.x, ev.y); if (action) return doAction(action, ev); } break; } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (placementSupport.mode === "wall") { // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities // itself happens in the call to updateBuildingPlacementPreview below.) placementSupport.wallSnapEntitiesIncludeOffscreen = false; } else { if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost })) { placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } if (isSnapToEdgesEnabled()) { // We need to reset the angle before the snapping to edges, // because we want to get the angle near to the default one. placementSupport.SetDefaultAngle(); } let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } updateBuildingPlacementPreview(); // includes an update of the snap entity candidates return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { if (placementSupport.mode === "wall") { let validPlacement = updateBuildingPlacementPreview(); if (validPlacement !== false) inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (isSnapToEdgesEnabled()) { let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_BUILDING_CLICK; } return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building. placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; case "hotkeydown": let rotation_step = Math.PI / 12; // 24 clicks make a full rotation switch (ev.hotkey) { case "session.rotate.cw": placementSupport.angle += rotation_step; updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementSupport.angle -= rotation_step; updateBuildingPlacementPreview(); break; } break; } break; case INPUT_FLARE: if (ev.type == "mousebuttondown") { if (ev.button == SDL_BUTTON_LEFT && controlsPlayer(g_ViewedPlayer)) { triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { inputState = INPUT_NORMAL; return true; } } } return false; } function doAction(action, ev) { if (!controlsPlayer(g_ViewedPlayer)) return false; return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action); } function popOneFromSelection(action) { // Pick the first unit that can do this order. let unit = action.firstAbleEntity || g_Selection.find(entity => ["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method => g_UnitActions[action.type][method] && g_UnitActions[action.type][method](action.target || undefined, [entity]) )); if (unit) { g_Selection.removeList([unit], true); return [unit]; } return null; } function positionUnitsFreehandSelectionMouseMove(ev) { // Converting the input line into a List of points. // For better performance the points must have a minimum distance to each other. let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); if (!g_FreehandSelection_InputLine.length || target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= g_FreehandSelection_ResolutionInputLineSquared) g_FreehandSelection_InputLine.push(target); return false; } function positionUnitsFreehandSelectionMouseUp(ev) { inputState = INPUT_NORMAL; let inputLine = g_FreehandSelection_InputLine; g_FreehandSelection_InputLine = []; if (ev.button != SDL_BUTTON_RIGHT) return true; let lengthOfLine = 0; for (let i = 1; i < inputLine.length; ++i) lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); // Checking the line for a minimum length to save performance. if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) { let action = determineAction(ev.x, ev.y); return !!action && doAction(action, ev); } // Even distribution of the units on the line. let p0 = inputLine[0]; let entityDistribution = [p0]; let distanceBetweenEnts = lengthOfLine / (selection.length - 1); let freeDist = -distanceBetweenEnts; for (let i = 1; i < inputLine.length; ++i) { let p1 = inputLine[i]; freeDist += inputLine[i - 1].distanceTo(p1); while (freeDist >= 0) { p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); entityDistribution.push(p0); freeDist -= distanceBetweenEnts; } } // Rounding errors can lead to missing or too many points. entityDistribution = entityDistribution.slice(0, selection.length); entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1])); if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) > Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0])) entityDistribution.reverse(); Engine.PostNetworkCommand({ "type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom", "entities": selection, "targetPositions": entityDistribution.map(pos => pos.toFixed(2)), "targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, "queued": Engine.HotkeyIsPressed("session.queue"), "pushFront": Engine.HotkeyIsPressed("session.pushorderfront"), "formation": NULL_FORMATION, }); // Add target markers with a minimum distance of 5 to each other. let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts); for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker) DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; } function triggerFlareAction(target) { let now = Date.now(); if (g_LastFlareTime && now < g_LastFlareTime + g_FlareCooldown) return; g_LastFlareTime = now; displayFlare(target, Engine.GetPlayerID()); Engine.PlayUISound(g_FlareSound, false); Engine.PostNetworkCommand({ "type": "map-flare", "target": target }); } function handleUnitAction(target, action) { if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute) { error("Invalid action.type " + action.type); return false; } let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection(action) || g_Selection.toList(); // If the session.queue hotkey is down, add the order to the unit's order queue instead // of running it immediately. If the pushorderfront hotkey is down, execute the order // immidiately and continue the rest of the queue afterwards. return g_UnitActions[action.type].execute( target, action, selection, Engine.HotkeyIsPressed("session.queue"), Engine.HotkeyIsPressed("session.pushorderfront")); } function getEntityLimitAndCount(playerState, entType) { let ret = { "entLimit": undefined, "entCount": undefined, "entLimitChangers": undefined, "canBeAddedCount": undefined, "matchLimit": undefined, "matchCount": undefined, "type": undefined }; if (!playerState.entityLimits) return ret; let template = GetTemplateData(entType); let entCategory; let matchLimit; if (template.trainingRestrictions) { entCategory = template.trainingRestrictions.category; matchLimit = template.trainingRestrictions.matchLimit; ret.type = "training"; } else if (template.buildRestrictions) { entCategory = template.buildRestrictions.category; matchLimit = template.buildRestrictions.matchLimit; ret.type = "build"; } if (entCategory && playerState.entityLimits[entCategory] !== undefined) { ret.entLimit = playerState.entityLimits[entCategory] || 0; ret.entCount = playerState.entityCounts[entCategory] || 0; ret.entLimitChangers = playerState.entityLimitChangers[entCategory]; ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0); } if (matchLimit) { ret.matchLimit = matchLimit; ret.matchCount = playerState.matchEntityCounts[entType] || 0; ret.canBeAddedCount = Math.min(Math.max(ret.entLimit - ret.entCount, 0), Math.max(ret.matchLimit - ret.matchCount, 0)); } return ret; } /** * Called by GUI when user clicks construction button. * @param {string} buildTemplate - Template name of the entity the user wants to build. */ function startBuildingPlacement(buildTemplate, playerState) { if (getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) return; // TODO: we should clear any highlight selection rings here. If the cursor was over an entity before going onto the GUI // to start building a structure, then the highlight selection rings are kept during the construction of the structure. // Gives the impression that somehow the hovered-over entity has something to do with the structure you're building. placementSupport.Reset(); let templateData = GetTemplateData(buildTemplate); if (templateData.wallSet) { placementSupport.mode = "wall"; placementSupport.wallSet = templateData.wallSet; inputState = INPUT_BUILDING_PLACEMENT; } else { placementSupport.mode = "building"; placementSupport.template = buildTemplate; inputState = INPUT_BUILDING_PLACEMENT; } if (templateData.attack && templateData.attack.Ranged && templateData.attack.Ranged.maxRange) placementSupport.attack = templateData.attack; } // Batch training: // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING // When the user releases shift, or clicks on a different training button, we create the batched units var g_BatchTrainingEntities; var g_BatchTrainingType; var g_NumberOfBatches; var g_BatchTrainingEntityAllowedCount; var g_BatchSize = getDefaultBatchTrainingSize(); function OnTrainMouseWheel(dir) { if (!Engine.HotkeyIsPressed("session.batchtrain")) return; g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio"); if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize)) g_BatchSize = 1; updateSelectionDetails(); } function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) { return entitiesToCheck.filter(entity => { - let state = GetEntityState(entity); - return state && state.production && state.production.entities.length && - state.production.entities.indexOf(trainEntType) != -1 && (!state.upgrade || !state.upgrade.isUpgrading); + const state = GetEntityState(entity); + return state?.trainer?.entities?.indexOf(trainEntType) != -1 && + (!state.upgrade || !state.upgrade.isUpgrading); }); } function initBatchTrain() { registerConfigChangeHandler(changes => { if (changes.has("gui.session.batchtrainingsize")) updateDefaultBatchSize(); }); } function getDefaultBatchTrainingSize() { let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize"); return Number.isInteger(num) && num > 0 ? num : 5; } function getBatchTrainingSize() { return Math.max(Math.round(g_BatchSize), 1); } function updateDefaultBatchSize() { g_BatchSize = getDefaultBatchTrainingSize(); } /** * Add the unit shown at position to the training queue for all entities in the selection. * @param {number} position - The position of the template to train. */ function addTrainingByPosition(position) { let playerState = GetSimState().players[Engine.GetPlayerID()]; let selection = g_Selection.toList(); if (!playerState || !selection.length) return; let trainableEnts = getAllTrainableEntitiesFromSelection(); let entToTrain = trainableEnts[position]; if (!entToTrain) return; addTrainingToQueue(selection, entToTrain, playerState); } // Called by GUI when user clicks training button function addTrainingToQueue(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; let decrement = Engine.HotkeyIsPressed("selection.remove"); let template; if (!decrement) template = GetTemplateData(trainEntType); // Batch training only possible if we can train at least 2 units. if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) { if (inputState == INPUT_BATCHTRAINING) { // Check if we are training in the same structure(s) as the last batch. // NOTE: We just check if the arrays are the same and if the order is the same. // If the order changed, we have a new selection and we should create a new batch. // If we're already creating a batch of this unit (in the same structure(s)), then just extend it // (if training limits allow). if (g_BatchTrainingEntities.length == selection.length && g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && g_BatchTrainingType == trainEntType) { if (decrement) { --g_NumberOfBatches; if (g_NumberOfBatches <= 0) inputState = INPUT_NORMAL; } else if (canBeAddedCount == undefined || canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) { if (Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) })) return; ++g_NumberOfBatches; } g_BatchTrainingEntityAllowedCount = canBeAddedCount; return; } else if (!decrement) flushTrainingBatch(); } if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, getBatchTrainingSize()) })) return; inputState = INPUT_BATCHTRAINING; g_BatchTrainingEntities = selection; g_BatchTrainingType = trainEntType; g_BatchTrainingEntityAllowedCount = canBeAddedCount; g_NumberOfBatches = 1; } else { let buildingsForTraining = appropriateBuildings; if (canBeAddedCount !== undefined) buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); Engine.PostNetworkCommand({ "type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } } /** * Returns the number of units that will be present in a batch if the user clicks * the training button depending on the batch training modifier hotkey. */ function getTrainingStatus(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let nextBatchTrainingCount = 0; let canBeAddedCount; if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType) { nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize(); canBeAddedCount = g_BatchTrainingEntityAllowedCount; } else canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; // We need to calculate count after the next increment if possible. if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && Engine.HotkeyIsPressed("session.batchtrain")) nextBatchTrainingCount += getBatchTrainingSize(); nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); // If training limits don't allow us to train batchedSize in each appropriate structure, // train as many full batches as we can and the remainder in one more structure. let buildingsCountToTrainFullBatch = appropriateBuildings.length; let remainderToTrain = 0; if (canBeAddedCount !== undefined && canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) { buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); remainderToTrain = canBeAddedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; } function flushTrainingBatch() { let batchedSize = g_NumberOfBatches * getBatchTrainingSize(); let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); // If training limits don't allow us to train batchedSize in each appropriate structure. if (g_BatchTrainingEntityAllowedCount !== undefined && g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) { // Train as many full batches as we can. let buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / batchedSize); Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), "template": g_BatchTrainingType, "count": batchedSize, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); // Train remainer in one more structure. let remainer = g_BatchTrainingEntityAllowedCount % batchedSize; if (remainer) Engine.PostNetworkCommand({ "type": "train", "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], "template": g_BatchTrainingType, "count": remainer, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } else Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings, "template": g_BatchTrainingType, "count": batchedSize, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": let toSelect = []; g_Groups.update(); for (let ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) { let entState = GetEntityState(getEntityOrHolder(toSelect[0])); let position = entState.position; if (position && entState.visibility != "hidden") Engine.CameraMoveTo(position.x, position.z); } break; case "save": case "breakUp": g_Groups.groups[groupId].reset(); if (action == "save") g_Groups.addEntities(groupId, g_Selection.toList()); updateGroups(); break; } } var lastIdleUnit = 0; var currIdleClassIndex = 0; var lastIdleClasses = []; function resetIdleUnit() { lastIdleUnit = 0; currIdleClassIndex = 0; lastIdleClasses = []; } function findIdleUnit(classes) { let append = Engine.HotkeyIsPressed("selection.add"); let selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. if (selectall || classes.length != lastIdleClasses.length || !classes.every((v, i) => v === lastIdleClasses[i])) resetIdleUnit(); lastIdleClasses = classes; let data = { "viewedPlayer": g_ViewedPlayer, "excludeUnits": append ? g_Selection.toList() : [], // If the current idle class index is not 0, put the class at that index first. "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) }; if (!selectall) { data.limit = 1; data.prevUnit = lastIdleUnit; } let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (!idleUnits.length) { // TODO: display a message to indicate no more idle units, or something Engine.GuiInterfaceCall("PlaySoundForPlayer", { "name": "no_idle_unit" }); resetIdleUnit(); return; } if (!append) g_Selection.reset(); g_Selection.addList(idleUnits); if (selectall) return; lastIdleUnit = idleUnits[0]; let entityState = GetEntityState(lastIdleUnit); if (entityState.position) Engine.CameraMoveTo(entityState.position.x, entityState.position.z); // Move the idle class index to the first class an idle unit was found for. let indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; } function clearSelection() { if (inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING) { inputState = INPUT_NORMAL; placementSupport.Reset(); } else g_Selection.reset(); preSelectedAction = ACTION_NONE; } Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 26000) @@ -1,1282 +1,1282 @@ /** * Contains the layout and button settings per selection panel * * getItems returns a list of basic items used to fill the panel. * This method is obligated. If the items list is empty, the panel * won't be rendered. * * Then there's a loop over all items provided. In the loop, * the item and some other standard data is added to a data object. * * The standard data is * { * "i": index * "item": item coming from the getItems function * "playerState": playerState * "unitEntStates": states of the selected entities * "rowLength": rowLength * "numberOfItems": number of items that will be processed * "button": gui Button object * "icon": gui Icon object * "guiSelection": gui button Selection overlay * "countDisplay": gui caption space * } * * Then for every data object, the setupButton function is called which * sets the view and handlers of the button. */ // Cache some formation info // Available formations per player var g_AvailableFormations = new Map(); var g_FormationsInfo = new Map(); var g_SelectionPanels = {}; var g_SelectionPanelBarterButtonManager; g_SelectionPanels.Alert = { "getMaxNumberOfItems": function() { return 2; }, "getItems": function(unitEntStates) { return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : []; }, "setupButton": function(data) { data.button.onPress = function() { switch (data.item) { case "raise": raiseAlert(); return; case "end": endOfAlert(); return; } }; switch (data.item) { case "raise": data.icon.sprite = "stretched:session/icons/bell_level1.png"; data.button.tooltip = translate("Raise an alert!"); break; case "end": data.button.tooltip = translate("End of alert."); data.icon.sprite = "stretched:session/icons/bell_level0.png"; break; } data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength); return true; } }; g_SelectionPanels.Barter = { "getMaxNumberOfItems": function() { return 5; }, "rowLength": 5, "conflictsWith": ["Garrison"], "getItems": function(unitEntStates) { // If more than `rowLength` resources, don't display icons. if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetBarterableCodes().length > this.rowLength) return []; return g_ResourceData.GetBarterableCodes(); }, "setupButton": function(data) { if (g_SelectionPanelBarterButtonManager) { g_SelectionPanelBarterButtonManager.setViewedPlayer(data.player); g_SelectionPanelBarterButtonManager.update(); } return true; } }; g_SelectionPanels.Command = { "getMaxNumberOfItems": function() { return 6; }, "getItems": function(unitEntStates) { let commands = []; for (let command in g_EntityCommands) { let info = getCommandInfo(command, unitEntStates); if (info) { info.name = command; commands.push(info); } } return commands; }, "setupButton": function(data) { data.button.tooltip = data.item.tooltip; data.button.onPress = function() { if (data.item.callback) data.item.callback(data.item); else performCommand(data.unitEntStates, data.item.name); }; data.countDisplay.caption = data.item.count || ""; data.button.enabled = data.item.enabled == true; data.icon.sprite = "stretched:session/icons/" + data.item.icon; let size = data.button.size; // relative to the center ( = 50%) size.rleft = 50; size.rright = 50; // offset from the center calculation, count on square buttons, so size.bottom is the width too size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1); size.right = size.left + size.bottom; data.button.size = size; return true; } }; g_SelectionPanels.Construction = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function() { return getAllBuildableEntitiesFromSelection(); }, "setupButton": function(data) { let template = GetTemplateData(data.item, data.player); if (!template) return false; let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": data.player }); let neededResources; if (template.cost) neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, 1), "player": data.player }); data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); }; let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; let tooltips = [ getEntityNamesFormatted, getVisibleEntityClassesFormatted, getAurasTooltip, getEntityTooltip ].map(func => func(template)); tooltips.push( getEntityCostTooltip(template, data.player), getResourceDropsiteTooltip(template), getGarrisonTooltip(template), getTurretsTooltip(template), getPopulationBonusTooltip(template), showTemplateViewerOnRightClickTooltip(template) ); let limits = getEntityLimitAndCount(data.playerState, data.item); tooltips.push( formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; if (!technologyEnabled || limits.canBeAddedCount == 0) { data.button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; } else if (neededResources) { data.button.enabled = false; modifier += resourcesToAlphaMask(neededResources) + ":"; } else data.button.enabled = controlsPlayer(data.player); if (template.icon) data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; g_SelectionPanels.Formation = { "getMaxNumberOfItems": function() { return 15; }, "rowLength": 5, "conflictsWith": ["Garrison"], "getItems": function(unitEntStates) { if (unitEntStates.some(state => !hasClass(state, "Unit"))) return []; if (unitEntStates.every(state => !state.identity || !state.identity.hasSomeFormation)) return []; if (!g_AvailableFormations.has(unitEntStates[0].player)) g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player)); return g_AvailableFormations.get(unitEntStates[0].player).filter(formation => unitEntStates.some(state => !!state.identity && state.identity.formations.indexOf(formation) != -1)); }, "setupButton": function(data) { if (!g_FormationsInfo.has(data.item)) g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); let formationOk = canMoveSelectionIntoFormation(data.item); let unitIds = data.unitEntStates.map(state => state.id); let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": unitIds, "formationTemplate": data.item }); data.button.onPress = function() { performFormation(unitIds, data.item); }; data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item); let formationInfo = g_FormationsInfo.get(data.item); let tooltip = translate(formationInfo.name); let isDefaultFormation = g_AutoFormation.isDefault(data.item); if (data.item === NULL_FORMATION) tooltip += "\n" + (isDefaultFormation ? translate("Default formation is disabled.") : translate("Right-click to disable the default formation feature.")); else tooltip += "\n" + (isDefaultFormation ? translate("This is the default formation, used for movement orders.") : translate("Right-click to set this as the default formation.")); if (!formationOk && formationInfo.tooltip) tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red"); data.button.tooltip = tooltip; data.button.enabled = formationOk && controlsPlayer(data.player); let grayscale = formationOk ? "" : "grayscale:"; data.guiSelection.hidden = !formationSelected; data.countDisplay.hidden = !isDefaultFormation; data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Garrison = { "getMaxNumberOfItems": function() { return 12; }, "rowLength": 4, "conflictsWith": ["Barter"], "getItems": function(unitEntStates) { if (unitEntStates.every(state => !state.garrisonHolder)) return []; let groups = new EntityGroups(); for (let state of unitEntStates) if (state.garrisonHolder) groups.add(state.garrisonHolder.entities); return groups.getEntsGrouped(); }, "setupButton": function(data) { let entState = GetEntityState(data.item.ents[0]); let template = GetTemplateData(entState.template); if (!template) return false; data.button.onPress = function() { unloadTemplate(template.selectionGroupName || entState.template, entState.player); }; data.countDisplay.caption = data.item.ents.length || ""; let canUngarrison = controlsPlayer(data.player) || controlsPlayer(entState.player); data.button.enabled = canUngarrison; data.button.tooltip = (canUngarrison ? sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) }) + "\n" + translate("Single-click to unload 1. Shift-click to unload all of this type.") : getEntityNames(template)) + "\n" + sprintf(translate("Player: %(playername)s"), { "playername": g_Players[entState.player].name }); data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(entState.player, 160); data.button.sprite_disabled = data.button.sprite; // Selection panel buttons only appear disabled if they // also appear disabled to the owner of the structure. data.icon.sprite = (canUngarrison || g_IsObserver ? "" : "grayscale:") + "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Gate = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function(unitEntStates) { let hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked); let hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked); if (hideLocked && hideUnlocked) return []; return [ { "hidden": hideLocked, "tooltip": translate("Lock Gate"), "icon": "session/icons/lock_locked.png", "locked": true }, { "hidden": hideUnlocked, "tooltip": translate("Unlock Gate"), "icon": "session/icons/lock_unlocked.png", "locked": false } ]; }, "setupButton": function(data) { data.button.onPress = function() { lockGate(data.item.locked); }; data.button.tooltip = data.item.tooltip; data.button.enabled = controlsPlayer(data.player); data.guiSelection.hidden = data.item.hidden; data.icon.sprite = "stretched:" + data.item.icon; setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; g_SelectionPanels.Pack = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function(unitEntStates) { let checks = {}; for (let state of unitEntStates) { if (!state.pack) continue; if (state.pack.progress == 0) { if (state.pack.packed) checks.unpackButton = true; else checks.packButton = true; } else if (state.pack.packed) checks.unpackCancelButton = true; else checks.packCancelButton = true; } let items = []; if (checks.packButton) items.push({ "packing": false, "packed": false, "tooltip": translate("Pack"), "callback": function() { packUnit(true); } }); if (checks.unpackButton) items.push({ "packing": false, "packed": true, "tooltip": translate("Unpack"), "callback": function() { packUnit(false); } }); if (checks.packCancelButton) items.push({ "packing": true, "packed": false, "tooltip": translate("Cancel Packing"), "callback": function() { cancelPackUnit(true); } }); if (checks.unpackCancelButton) items.push({ "packing": true, "packed": true, "tooltip": translate("Cancel Unpacking"), "callback": function() { cancelPackUnit(false); } }); return items; }, "setupButton": function(data) { data.button.onPress = function() {data.item.callback(data.item); }; data.button.tooltip = data.item.tooltip; if (data.item.packing) data.icon.sprite = "stretched:session/icons/cancel.png"; else if (data.item.packed) data.icon.sprite = "stretched:session/icons/unpack.png"; else data.icon.sprite = "stretched:session/icons/pack.png"; data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; g_SelectionPanels.Queue = { "getMaxNumberOfItems": function() { return 16; }, /** * Returns a list of all items in the productionqueue of the selection * The first entry of every entity's production queue will come before * the second entry of every entity's production queue */ "getItems": function(unitEntStates) { const queue = []; let foundNew = true; for (let i = 0; foundNew; ++i) { foundNew = false; for (const state of unitEntStates) { if (!state.production || !state.production.queue[i]) continue; queue.push({ "producingEnt": state.id, "queuedItem": state.production.queue[i], "autoqueue": state.production.autoqueue && state.production.queue[i].unitTemplate, }); foundNew = true; } } if (!queue.length) return queue; // Add 'ghost' items to show autoqueues. const repeat = []; for (const item of queue) if (item.autoqueue) { const ghostItem = clone(item); ghostItem.ghost = true; repeat.push(ghostItem); } if (repeat.length) for (let i = 0; queue.length < g_SelectionPanels.Queue.getMaxNumberOfItems(); ++i) queue.push(repeat[i % repeat.length]); return queue; }, "resizePanel": function(numberOfItems, rowLength) { let numRows = Math.ceil(numberOfItems / rowLength); let panel = Engine.GetGUIObjectByName("unitQueuePanel"); let size = panel.size; let buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom; let margin = 4; size.top = size.bottom - numRows * buttonSize - (numRows + 2) * margin; panel.size = size; }, "setupButton": function(data) { let queuedItem = data.item.queuedItem; // Differentiate between units and techs let template; if (queuedItem.unitTemplate) template = GetTemplateData(queuedItem.unitTemplate); else if (queuedItem.technologyTemplate) template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ); else { warning("Unknown production queue template " + uneval(queuedItem)); return false; } data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); }; const tooltips = [getEntityNames(template)]; if (data.item.ghost) tooltips.push(translate("The auto-queue will try to train this item later.")); if (queuedItem.neededSlots) { tooltips.push(coloredText(translate("Insufficient population capacity:"), "red")); tooltips.push(sprintf(translate("%(population)s %(neededSlots)s"), { "population": resourceIcon("population"), "neededSlots": queuedItem.neededSlots })); } tooltips.push(showTemplateViewerOnRightClickTooltip(template)); data.button.tooltip = tooltips.join("\n"); data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : ""; if (data.item.ghost) { data.button.enabled = false; Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]").sprite="color:0 150 250 50"; } else { // Show the time remaining to finish the first item if (data.i == 0) Engine.GetGUIObjectByName("queueTimeRemaining").caption = Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss")); const guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]"); guiObject.sprite = "queueProgressSlider"; const size = guiObject.size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left)); guiObject.size = size; data.button.enabled = controlsPlayer(data.player); Engine.GetGUIObjectByName("unitQueuePausedIcon[" + data.i + "]").hidden = !queuedItem.paused; if (queuedItem.paused) // Translation: String displayed when the research is paused. E.g. by being garrisoned or when not the first item in the queue. data.button.tooltip += "\n" + translate("(This item is paused.)"); } if (template.icon) { let modifier = "stretched:"; if (queuedItem.paused) modifier += "color:0 0 0 127:grayscale:"; else if (data.item.ghost) modifier += "grayscale:"; data.icon.sprite = modifier + "session/portraits/" + template.icon; } const showTemplateFunc = () => { showTemplateDetails(data.item.queuedItem.unitTemplate || data.item.queuedItem.technologyTemplate, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Research = { "getMaxNumberOfItems": function() { return 10; }, "rowLength": 10, "getItems": function(unitEntStates) { let ret = []; if (unitEntStates.length == 1) - return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret : - unitEntStates[0].production.technologies.map(tech => ({ + return !unitEntStates[0].researcher || !unitEntStates[0].researcher.technologies ? ret : + unitEntStates[0].researcher.technologies.map(tech => ({ "tech": tech, - "techCostMultiplier": unitEntStates[0].production.techCostMultiplier, + "techCostMultiplier": unitEntStates[0].researcher.techCostMultiplier, "researchFacilityId": unitEntStates[0].id, "isUpgrading": !!unitEntStates[0].upgrade && unitEntStates[0].upgrade.isUpgrading })); let sortedEntStates = unitEntStates.sort((a, b) => (!b.upgrade || !b.upgrade.isUpgrading) - (!a.upgrade || !a.upgrade.isUpgrading) || (!a.production ? 0 : a.production.queue.length) - (!b.production ? 0 : b.production.queue.length) ); for (let state of sortedEntStates) { - if (!state.production || !state.production.technologies) + if (!state.researcher || !state.researcher.technologies) continue; // Remove the techs we already have in ret (with the same name and techCostMultiplier) - let filteredTechs = state.production.technologies.filter( + const filteredTechs = state.researcher.technologies.filter( tech => tech != null && !ret.some( item => (item.tech == tech || item.tech.pair && tech.pair && item.tech.bottom == tech.bottom && item.tech.top == tech.top) && Object.keys(item.techCostMultiplier).every( - k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k]) + k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k]) )); if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() && getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2)) ret = ret.concat(filteredTechs.map(tech => ({ "tech": tech, - "techCostMultiplier": state.production.techCostMultiplier, + "techCostMultiplier": state.researcher.techCostMultiplier, "researchFacilityId": state.id, "isUpgrading": !!state.upgrade && state.upgrade.isUpgrading }))); } return ret; }, "hideItem": function(i, rowLength) // Called when no item is found { Engine.GetGUIObjectByName("unitResearchButton[" + i + "]").hidden = true; // We also remove the paired tech and the pair symbol Engine.GetGUIObjectByName("unitResearchButton[" + (i + rowLength) + "]").hidden = true; Engine.GetGUIObjectByName("unitResearchPair[" + i + "]").hidden = true; }, "setupButton": function(data) { if (!data.item.tech) { g_SelectionPanels.Research.hideItem(data.i, data.rowLength); return false; } // Start position (start at the bottom) let position = data.i + data.rowLength; // Only show the top button for pairs if (!data.item.tech.pair) Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; // Set up the tech connector let pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]"); pair.hidden = data.item.tech.pair == null; setPanelObjectPosition(pair, data.i, data.rowLength); // Handle one or two techs (tech pair) let player = data.player; let playerState = GetSimState().players[player]; for (let tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech]) { // Don't change the object returned by GetTechnologyData let template = clone(GetTechnologyData(tech, playerState.civ)); if (!template) return false; // Not allowed by civ. if (!template.reqs) { // One of the pair may still be researchable by the current civ, // hence don't hide everything. Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; pair.hidden = true; continue; } for (let res in template.cost) template.cost[res] *= data.item.techCostMultiplier[res]; let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": template.cost, "player": player }); let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", { "tech": tech, "player": player }); let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]"); let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]"); let tooltips = [ getEntityNamesFormatted, getEntityTooltip, getEntityCostTooltip, showTemplateViewerOnRightClickTooltip ].map(func => func(template)); if (!requirementsPassed) { let tip = template.requirementsTooltip; let reqs = template.reqs; for (let req of reqs) { if (!req.entities) continue; let entityCounts = []; for (let entity of req.entities) { let current = 0; switch (entity.check) { case "count": current = playerState.classCounts[entity.class] || 0; break; case "variants": current = playerState.typeCountsByClass[entity.class] ? Object.keys(playerState.typeCountsByClass[entity.class]).length : 0; break; } let remaining = entity.number - current; if (remaining < 1) continue; entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), { "number": remaining, "class": translate(entity.class) })); } tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), { "entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", ")) }); } tooltips.push(tip); } tooltips.push(getNeededResourcesTooltip(neededResources)); button.tooltip = tooltips.filter(tip => tip).join("\n"); button.onPress = (t => function() { addResearchToQueue(data.item.researchFacilityId, t); })(tech); let showTemplateFunc = (t => function() { showTemplateDetails( t, GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv); }); button.onPressRight = showTemplateFunc(tech); button.onPressRightDisabled = showTemplateFunc(tech); if (data.item.tech.pair) { // On mouse enter, show a cross over the other icon let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]"); button.onMouseEnter = function() { unchosenIcon.hidden = false; }; button.onMouseLeave = function() { unchosenIcon.hidden = true; }; } button.hidden = false; let modifier = ""; if (!requirementsPassed) { button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; } else if (neededResources) { button.enabled = false; modifier += resourcesToAlphaMask(neededResources) + ":"; } else button.enabled = controlsPlayer(data.player); if (data.item.isUpgrading) { button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; button.tooltip += "\n" + coloredText(translate("Cannot research while upgrading."), "red"); } if (template.icon) icon.sprite = modifier + "stretched:session/portraits/" + template.icon; setPanelObjectPosition(button, position, data.rowLength); // Prepare to handle the top button (if any) position -= data.rowLength; } return true; } }; g_SelectionPanels.Selection = { "getMaxNumberOfItems": function() { return 16; }, "rowLength": 4, "getItems": function(unitEntStates) { if (unitEntStates.length < 2) return []; return g_Selection.groups.getEntsGrouped(); }, "setupButton": function(data) { let entState = GetEntityState(data.item.ents[0]); let template = GetTemplateData(entState.template); if (!template) return false; for (let ent of data.item.ents) { let state = GetEntityState(ent); if (state.resourceCarrying && state.resourceCarrying.length !== 0) { if (!data.carried) data.carried = {}; let carrying = state.resourceCarrying[0]; if (data.carried[carrying.type]) data.carried[carrying.type] += carrying.amount; else data.carried[carrying.type] = carrying.amount; } if (state.trader && state.trader.goods && state.trader.goods.amount) { if (!data.carried) data.carried = {}; let amount = state.trader.goods.amount; let type = state.trader.goods.type; let totalGain = amount.traderGain; if (amount.market1Gain) totalGain += amount.market1Gain; if (amount.market2Gain) totalGain += amount.market2Gain; if (data.carried[type]) data.carried[type] += totalGain; else data.carried[type] = totalGain; } } let unitOwner = GetEntityState(data.item.ents[0]).player; let tooltip = getEntityNames(template); if (data.carried) tooltip += "\n" + Object.keys(data.carried).map(res => resourceIcon(res) + data.carried[res] ).join(" "); if (g_IsObserver) tooltip += "\n" + sprintf(translate("Player: %(playername)s"), { "playername": g_Players[unitOwner].name }); data.button.tooltip = tooltip; data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(unitOwner, 160); data.guiSelection.hidden = !g_IsObserver; data.countDisplay.caption = data.item.ents.length || ""; data.button.onPress = function() { if (Engine.HotkeyIsPressed("session.deselectgroup")) removeFromSelectionGroup(data.item.key); else makePrimarySelectionGroup(data.item.key); }; data.button.onPressRight = function() { removeFromSelectionGroup(data.item.key); }; if (template.icon) data.icon.sprite = "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Stance = { "getMaxNumberOfItems": function() { return 5; }, "getItems": function(unitEntStates) { if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal"))) return []; return unitEntStates[0].unitAI.selectableStances; }, "setupButton": function(data) { let unitIds = data.unitEntStates.map(state => state.id); data.button.onPress = function() { performStance(unitIds, data.item); }; data.button.tooltip = getStanceDisplayName(data.item) + "\n" + "[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]"; data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", { "ents": unitIds, "stance": data.item }); data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png"; data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Training = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function() { return getAllTrainableEntitiesFromSelection(); }, "setupButton": function(data) { let template = GetTemplateData(data.item, data.player); if (!template) return false; let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": data.player }); let unitIds = data.unitEntStates.map(status => status.id); let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingStatus(unitIds, data.item, data.playerState); let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; let neededResources; if (template.cost) neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, trainNum), "player": data.player }); data.button.onPress = function() { if (!neededResources) addTrainingToQueue(unitIds, data.item, data.playerState); }; let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; data.countDisplay.caption = trainNum > 1 ? trainNum : ""; let tooltips = [ "[font=\"sans-bold-16\"]" + colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) + "[/font]" + " " + getEntityNamesFormatted(template), getVisibleEntityClassesFormatted(template), getAurasTooltip(template), getEntityTooltip(template), getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) ]; let limits = getEntityLimitAndCount(data.playerState, data.item); tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type)); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") tooltips = tooltips.concat([ getHealthTooltip, getAttackTooltip, getHealerTooltip, getResistanceTooltip, getGarrisonTooltip, getTurretsTooltip, getProjectilesTooltip, getSpeedTooltip, getResourceDropsiteTooltip ].map(func => func(template))); tooltips.push(showTemplateViewerOnRightClickTooltip()); tooltips.push( formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch), getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; if (!technologyEnabled || limits.canBeAddedCount == 0) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } else { data.button.enabled = controlsPlayer(data.player); if (neededResources) modifier = resourcesToAlphaMask(neededResources) + ":"; } if (data.unitEntStates.every(state => state.upgrade && state.upgrade.isUpgrading)) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; data.button.tooltip += "\n" + coloredText(translate("Cannot train while upgrading."), "red"); } if (template.icon) data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon; let index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); return true; } }; g_SelectionPanels.Upgrade = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function(unitEntStates) { // Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units. if (unitEntStates.some(state => state.template != unitEntStates[0].template)) return false; return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades; }, "setupButton": function(data) { let template = GetTemplateData(data.item.entity); if (!template) return false; let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]"); progressOverlay.hidden = true; let technologyEnabled = true; if (data.item.requiredTechnology) technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.item.requiredTechnology, "player": data.player }); let limits = getEntityLimitAndCount(data.playerState, data.item.entity); let upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.template == data.item.entity); let upgradableEntStates = data.unitEntStates.filter(state => !state.upgrade.progress && (!state.production || !state.production.queue || !state.production.queue.length)); let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.item, upgradableEntStates.length), "player": data.player }); let tooltip; let modifier = ""; if (!upgradingEntStates.length && upgradableEntStates.length) { let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic; let secondaryName; if (g_ShowSecondaryNames) secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific; let tooltips = []; if (g_ShowSecondaryNames) { if (data.item.tooltip) tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s). %(tooltip)s"), { "primaryName": primaryName, "secondaryName": secondaryName, "tooltip": translate(data.item.tooltip) })); else tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s)."), { "primaryName": primaryName, "secondaryName": secondaryName })); } else { if (data.item.tooltip) tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s. %(tooltip)s"), { "primaryName": primaryName, "tooltip": translate(data.item.tooltip) })); else tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s."), { "primaryName": primaryName })); } tooltips.push( getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length), formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources), showTemplateViewerOnRightClickTooltip()); tooltip = tooltips.filter(tip => tip).join("\n"); data.button.onPress = function() { upgradeEntity( data.item.entity, upgradableEntStates.map(state => state.id)); }; if (!technologyEnabled || limits.canBeAddedCount == 0 && !upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.template))) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } else if (neededResources) { data.button.enabled = false; modifier = resourcesToAlphaMask(neededResources) + ":"; } data.countDisplay.caption = upgradableEntStates.length > 1 ? upgradableEntStates.length : ""; } else if (upgradingEntStates.length) { tooltip = translate("Cancel Upgrading"); data.button.onPress = function() { cancelUpgradeEntity(); }; data.countDisplay.caption = upgradingEntStates.length > 1 ? upgradingEntStates.length : ""; let progress = 0; for (let state of upgradingEntStates) progress = Math.max(progress, state.upgrade.progress || 1); let progressOverlaySize = progressOverlay.size; // TODO This is bad: we assume the progressOverlay is square progressOverlaySize.top = progressOverlaySize.bottom + Math.round((1 - progress) * (progressOverlaySize.left - progressOverlaySize.right)); progressOverlay.size = progressOverlaySize; progressOverlay.hidden = false; } else { tooltip = coloredText(translatePlural( "Cannot upgrade when the entity is training, researching or already upgrading.", "Cannot upgrade when all entities are training, researching or already upgrading.", data.unitEntStates.length), "red"); data.button.onPress = function() {}; data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } data.button.enabled = controlsPlayer(data.player); data.button.tooltip = tooltip; let showTemplateFunc = () => { showTemplateDetails(data.item.entity, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; data.icon.sprite = modifier + "stretched:session/" + (data.item.icon || "portraits/" + template.icon); setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; function initSelectionPanels() { let unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel"); if (BarterButtonManager.IsAvailable(unitBarterPanel)) g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel); } /** * Pauses game and opens the template details viewer for a selected entity or technology. * * Technologies don't have a set civ, so we pass along the native civ of * the template of the entity that's researching it. * * @param {string} [civCode] - The template name of the entity that researches the selected technology. */ function showTemplateDetails(templateName, civCode) { if (inputState != INPUT_NORMAL) return; g_PauseControl.implicitPause(); Engine.PushGuiPage( "page_viewer.xml", { "templateName": templateName, "civ": civCode }, resumeGame); } /** * If two panels need the same space, so they collide, * the one appearing first in the order is rendered. * * Note that the panel needs to appear in the list to get rendered. */ let g_PanelsOrder = [ // LEFT PANE "Barter", // Must always be visible on markets "Garrison", // More important than Formation, as you want to see the garrisoned units in ships "Alert", "Formation", "Stance", // Normal together with formation // RIGHT PANE "Gate", // Must always be shown on gates "Pack", // Must always be shown on packable entities "Upgrade", // Must always be shown on upgradable entities "Training", "Construction", "Research", // Normal together with training // UNIQUE PANES (importance doesn't matter) "Command", "Queue", "Selection", ]; Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 26000) @@ -1,577 +1,577 @@ /** * @file Contains all helper functions that are needed only for selection_panels.js * and some that are needed for hotkeys, but not for anything inside input.js. */ const UPGRADING_NOT_STARTED = -2; const UPGRADING_CHOSEN_OTHER = -1; function canMoveSelectionIntoFormation(formationTemplate) { if (formationTemplate == NULL_FORMATION) return true; if (!(formationTemplate in g_canMoveIntoFormation)) g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", { "ents": g_Selection.toList(), "formationTemplate": formationTemplate }); return g_canMoveIntoFormation[formationTemplate]; } function hasSameRestrictionCategory(templateName1, templateName2) { let template1 = GetTemplateData(templateName1); let template2 = GetTemplateData(templateName2); if (template1.trainingRestrictions && template2.trainingRestrictions) return template1.trainingRestrictions.category == template2.trainingRestrictions.category; if (template1.buildRestrictions && template2.buildRestrictions) return template1.buildRestrictions.category == template2.buildRestrictions.category; return false; } /** * Returns a "color:255 0 0 Alpha" string based on how many resources are needed. */ function resourcesToAlphaMask(neededResources) { let totalCost = 0; for (let resource in neededResources) totalCost += +neededResources[resource]; return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50); } function getStanceDisplayName(name) { switch (name) { case "violent": return translateWithContext("stance", "Violent"); case "aggressive": return translateWithContext("stance", "Aggressive"); case "defensive": return translateWithContext("stance", "Defensive"); case "passive": return translateWithContext("stance", "Passive"); case "standground": return translateWithContext("stance", "Standground"); default: warn("Internationalization: Unexpected stance found: " + name); return name; } } function getStanceTooltip(name) { switch (name) { case "violent": return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible"); case "aggressive": return translateWithContext("stance", "Attack nearby opponents"); case "defensive": return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location"); case "passive": return translateWithContext("stance", "Flee if attacked"); case "standground": return translateWithContext("stance", "Attack opponents in range, but don't move"); default: return ""; } } /** * Format entity count/limit message for the tooltip */ function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers) { if (trainEntLimit == undefined) return ""; var text = sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), { "count": trainEntCount, "limit": trainEntLimit }); if (trainEntCount >= trainEntLimit) text = coloredText(text, "red"); for (var c in trainEntLimitChangers) { if (!trainEntLimitChangers[c]) continue; let string = trainEntLimitChangers[c] > 0 ? translate("%(changer)s enlarges the limit with %(change)s.") : translate("%(changer)s lessens the limit with %(change)s."); text += "\n" + sprintf(string, { "changer": translate(c), "change": trainEntLimitChangers[c] }); } return text; } /** * Format template match count/limit message for the tooltip. * * @param {number} matchEntLimit - The limit of the entity. * @param {number} matchEntCount - The count of the entity. * @param {string} type - The type of the action (i.e. "build" or "training"). * * @return {string} - The string to show the user with information regarding the limit of this template. */ function formatMatchLimitString(matchEntLimit, matchEntCount, type) { if (matchEntLimit == undefined) return ""; let passedLimit = matchEntCount >= matchEntLimit; let count = matchEntLimit - matchEntCount; let text; if (type == "build") { if (passedLimit) text = sprintf(translatePlural("Could only be constructed once.", "Could only be constructed %(limit)s times.", matchEntLimit), { "limit": matchEntLimit }); else if (matchEntLimit == 1) text = translate("Can be constructed only once."); else text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), { "count": count }); } else if (type == "training") { if (passedLimit) text = sprintf(translatePlural("Could only be trained once.", "Could only be trained %(limit)s times.", matchEntLimit), { "limit": matchEntLimit }); else if (matchEntLimit == 1) text = translate("Can be trained only once."); else text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), { "count": count }); } else { if (passedLimit) text = sprintf(translatePlural("Could only be created once.", "Could only be created %(limit)s times.", matchEntLimit), { "limit": matchEntLimit }); else if (matchEntLimit == 1) text = translate("Can be created only once."); else text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), { "count": count }); } return passedLimit ? coloredText(text, "red") : text; } /** * Format batch training string for the tooltip * Examples: * buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0: * "Shift-click to train 5" * buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0: * "Shift-click to train 10 (2*5)" * buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12: * "Shift-click to train 27 (15 + 12)" */ function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) { var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; // Don't show the batch training tooltip if either units of this type can't be trained at all // or only one unit can be trained if (totalBatchTrainingCount < 2) return ""; let fullBatchesString = ""; if (buildingsCountToTrainFullBatch > 1) fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), { "buildings": buildingsCountToTrainFullBatch, "batchSize": fullBatchSize }); else if (buildingsCountToTrainFullBatch == 1) fullBatchesString = fullBatchSize; // We need to display the batch details part if there is either more than // one structure with full batch or one structure with the full batch and // another with a partial batch let batchString; if (buildingsCountToTrainFullBatch > 1 || buildingsCountToTrainFullBatch == 1 && remainderBatch > 0) if (remainderBatch > 0) batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s)."); else batchString = translate("%(action)s to train %(number)s (%(fullBatch)s)."); else batchString = translate("%(action)s to train %(number)s."); return "[font=\"sans-13\"]" + setStringTags( sprintf(batchString, { "action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]", "number": totalBatchTrainingCount, "fullBatch": fullBatchesString, "remainderBatch": remainderBatch }), g_HotkeyTags) + "[/font]"; } /** * Camera jumping: when the user presses a hotkey the current camera location is marked. * When pressing another camera jump hotkey the camera jumps back to that position. * When the camera is already roughly at that location, jump back to where it was previously. */ var g_JumpCameraPositions = []; var g_JumpCameraLast; function jumpCamera(index) { let position = g_JumpCameraPositions[index]; if (!position) return; let threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold"); let cameraPivot = Engine.GetCameraPivot(); if (g_JumpCameraLast && Math.abs(cameraPivot.x - position.x) < threshold && Math.abs(cameraPivot.z - position.z) < threshold) { Engine.CameraMoveTo(g_JumpCameraLast.x, g_JumpCameraLast.z); } else { g_JumpCameraLast = cameraPivot; Engine.CameraMoveTo(position.x, position.z); } } function setJumpCamera(index) { g_JumpCameraPositions[index] = Engine.GetCameraPivot(); } /** * Called by GUI when user clicks a research button. */ function addResearchToQueue(entity, researchType) { Engine.PostNetworkCommand({ "type": "research", "entity": entity, "template": researchType, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } /** * Called by GUI when user clicks a production queue item. */ function removeFromProductionQueue(entity, id) { Engine.PostNetworkCommand({ "type": "stop-production", "entity": entity, "id": id }); } /** * Called by unit selection buttons. */ function makePrimarySelectionGroup(templateName) { g_Selection.makePrimarySelection(templateName); } function removeFromSelectionGroup(templateName) { g_Selection.removeGroupFromSelection(templateName); } function performCommand(entStates, commandName) { if (!entStates.length) return; if (getCommandInfo(commandName, entStates)) g_EntityCommands[commandName].execute(entStates); } function performFormation(entities, formationTemplate) { if (!entities) return; Engine.PostNetworkCommand({ "type": "formation", "entities": entities, "formation": formationTemplate }); } function performStance(entities, stanceName) { if (!entities) return; Engine.PostNetworkCommand({ "type": "stance", "entities": entities, "name": stanceName }); } function lockGate(lock) { Engine.PostNetworkCommand({ "type": "lock-gate", "entities": g_Selection.toList(), "lock": lock }); } function packUnit(pack) { Engine.PostNetworkCommand({ "type": "pack", "entities": g_Selection.toList(), "pack": pack, "queued": false }); } function cancelPackUnit(pack) { Engine.PostNetworkCommand({ "type": "cancel-pack", "entities": g_Selection.toList(), "pack": pack, "queued": false }); } function upgradeEntity(Template, selection) { Engine.PostNetworkCommand({ "type": "upgrade", "entities": selection, "template": Template, "queued": false }); } function cancelUpgradeEntity() { Engine.PostNetworkCommand({ "type": "cancel-upgrade", "entities": g_Selection.toList(), "queued": false }); } /** * Set the camera to follow the given entity if it's a unit. * Otherwise stop following. */ function setCameraFollow(entity) { let entState = entity && GetEntityState(entity); if (entState && hasClass(entState, "Unit")) Engine.CameraFollow(entity); else Engine.CameraFollow(0); } function stopUnits(entities) { Engine.PostNetworkCommand({ "type": "stop", "entities": entities, "queued": false }); } function unloadTemplate(template, owner) { Engine.PostNetworkCommand({ "type": "unload-template", "all": Engine.HotkeyIsPressed("session.unloadtype"), "template": template, "owner": owner, // Filter out all entities that aren't garrisonable. "garrisonHolders": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && !!state.garrisonHolder; }) }); } function unloadAll() { const garrisonHolders = g_Selection.filter(e => { let state = GetEntityState(e); return state && !!state.garrisonHolder; }); if (!garrisonHolders.length) return; let ownEnts = []; let otherEnts = []; for (let ent of garrisonHolders) { if (controlsPlayer(GetEntityState(ent).player)) ownEnts.push(ent); else otherEnts.push(ent); } if (ownEnts.length) Engine.PostNetworkCommand({ "type": "unload-all", "garrisonHolders": ownEnts }); if (otherEnts.length) Engine.PostNetworkCommand({ "type": "unload-all-by-owner", "garrisonHolders": otherEnts }); } function unloadAllTurrets() { const turretHolders = g_Selection.filter(e => { let state = GetEntityState(e); return state && !!state.turretHolder; }); if (!turretHolders.length) return; let ownedHolders = []; let ejectables = []; for (let ent of turretHolders) { let turretHolderState = GetEntityState(ent); if (controlsPlayer(turretHolderState.player)) ownedHolders.push(ent); else { for (let turret of turretHolderState.turretHolder.turretPoints.map(tp => tp.entity)) if (turret && controlsPlayer(GetEntityState(turret).player)) ejectables.push(turret); } } if (ejectables.length) Engine.PostNetworkCommand({ "type": "leave-turret", "entities": ejectables }); if (ownedHolders.length) Engine.PostNetworkCommand({ "type": "unload-turrets", "entities": ownedHolders }); } function leaveTurretPoints() { const entities = g_Selection.filter(entity => { let entState = GetEntityState(entity); return entState && entState.turretable && entState.turretable.holder != INVALID_ENTITY; }); Engine.PostNetworkCommand({ "type": "leave-turret", "entities": entities }); } function backToWork() { Engine.PostNetworkCommand({ "type": "back-to-work", // Filter out all entities that can't go back to work. "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && state.unitAI && state.unitAI.hasWorkOrders; }) }); } function removeGuard() { Engine.PostNetworkCommand({ "type": "remove-guard", // Filter out all entities that are currently guarding/escorting. "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && state.unitAI && state.unitAI.isGuarding; }) }); } function raiseAlert() { Engine.PostNetworkCommand({ "type": "alert-raise", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && !!state.alertRaiser; }) }); } function endOfAlert() { Engine.PostNetworkCommand({ "type": "alert-end", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && !!state.alertRaiser; }) }); } function turnAutoQueueOn() { Engine.PostNetworkCommand({ "type": "autoqueue-on", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); - return !!state?.production?.entities.length && + return !!state?.trainer?.entities?.length && !state.production.autoqueue; }) }); } function turnAutoQueueOff() { Engine.PostNetworkCommand({ "type": "autoqueue-off", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); - return !!state?.production?.entities.length && + return !!state?.trainer?.entities?.length && state.production.autoqueue; }) }); } Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 26000) @@ -1,1957 +1,1957 @@ /** * Specifies which template should indicate the target location of a player command, * given a command type. */ var g_TargetMarker = { "move": "special/target_marker", "map_flare": "special/flare_target_marker" }; /** * Sound we play when displaying a flare. */ var g_FlareSound = "audio/interface/alarm/alarmally_1.ogg"; /** * Which enemy entity types will be attacked on sight when patroling. */ var g_PatrolTargets = ["Unit"]; const g_DisabledTags = { "color": "255 140 0" }; /** * List of different actions units can execute, * this is mostly used to determine which actions can be executed * * "execute" is meant to send the command to the engine * * The next functions will always return false * in case you have to continue to seek * (i.e. look at the next entity for getActionInfo, the next * possible action for the actionCheck ...) * They will return an object when the searching is finished * * "getActionInfo" is used to determine if the action is possible, * and also give visual feedback to the user (tooltips, cursors, ...) * * "preSelectedActionCheck" is used to select actions when the gui buttons * were used to set them, but still require a target (like the guard button) * * "hotkeyActionCheck" is used to check the possibility of actions when * a hotkey is pressed * * "actionCheck" is used to check the possibilty of actions without specific * command. For that, the specificness variable is used * * "specificness" is used to determine how specific an action is, * The lower the number, the more specific an action is, and the bigger * the chance of selecting that action when multiple actions are possible */ var g_UnitActions = { "move": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getDefault() }); DrawTargetMarker(target); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.move") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("move", target, selection); return actionInfo.possible && { "type": "move", "firstAbleEntity": actionInfo.entity }; }, "specificness": 12, }, "attack-move": { "execute": function(target, action, selection, queued, pushFront) { let targetClasses; if (Engine.HotkeyIsPressed("session.attackmoveUnit")) targetClasses = { "attack": ["Unit"] }; else targetClasses = { "attack": ["Unit", "Structure"] }; Engine.PostNetworkCommand({ "type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); DrawTargetMarker(target); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return isAttackMovePressed() && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("attack-move", target, selection); return actionInfo.possible && { "type": "attack-move", "cursor": "action-attack-move", "firstAbleEntity": actionInfo.entity }; }, "specificness": 30, }, "capture": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "allowCapture": true, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.attack || !targetState || !targetState.capturePoints) return false; return { "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, "target": targetState.id, "types": ["Capture"] }) }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("capture", target, selection); return actionInfo.possible && { "type": "capture", "cursor": "action-capture", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 9, }, "attack": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "attack", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "allowCapture": false, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.attack || !targetState || !targetState.hitpoints) return false; return { "possible": Engine.GuiInterfaceCall("CanAttack", { "entity": entState.id, "target": targetState.id, "types": ["!Capture"] }) }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.attack") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("attack", target, selection); return actionInfo.possible && { "type": "attack", "cursor": "action-attack", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 10, }, "call-to-arms": { "execute": function(target, action, selection, queued, pushFront) { let targetClasses; if (Engine.HotkeyIsPressed("session.attackmoveUnit")) targetClasses = { "attack": ["Unit"] }; else targetClasses = { "attack": ["Unit", "Structure"] }; Engine.PostNetworkCommand({ "type": "call-to-arms", "entities": selection, "target": target, "targetClasses": targetClasses, "queued": queued, "pushFront": pushFront, "allowCapture": true, "formation": g_AutoFormation.getNull() }); return true; }, "getActionInfo": function(entState, targetState) { return { "possible": !!entState.unitAI }; }, "actionCheck": function(target, selection) { const actionInfo = getActionInfo("call-to-arms", target, selection); return actionInfo.possible && { "type": "call-to-arms", "cursor": "action-attack", "target": target, "firstAbleEntity": actionInfo.entity }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.calltoarms") && this.actionCheck(target, selection); }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_CALLTOARMS && this.actionCheck(target, selection); }, "specificness": 50, }, "patrol": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "patrol", "entities": selection, "x": target.x, "z": target.z, "target": action.target, "targetClasses": { "attack": g_PatrolTargets }, "queued": queued, "allowCapture": false, "formation": g_AutoFormation.getDefault() }); DrawTargetMarker(target); Engine.GuiInterfaceCall("PlaySound", { "name": "order_patrol", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI || !entState.unitAI.canPatrol) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.patrol") && this.actionCheck(target, selection); }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_PATROL && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("patrol", target, selection); return actionInfo.possible && { "type": "patrol", "cursor": "action-patrol", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 37, }, "heal": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "heal", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.heal || !targetState || !hasClass(targetState, "Unit") || !targetState.needsHeal || !playerCheck(entState, targetState, ["Player", "Ally"]) || entState.id == targetState.id) // Healers can't heal themselves. return false; let unhealableClasses = entState.heal.unhealableClasses; if (MatchesClassList(targetState.identity.classes, unhealableClasses)) return false; let healableClasses = entState.heal.healableClasses; if (!MatchesClassList(targetState.identity.classes, healableClasses)) return false; return { "possible": true }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("heal", target, selection); return actionInfo.possible && { "type": "heal", "cursor": "action-heal", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 7, }, // "Fake" action to check if an entity can be ordered to "construct" // which is handled differently from repair as the target does not exist. "construct": { "preSelectedActionCheck": function(target, selection) { let state = GetEntityState(selection[0]); if (state && state.builder && target && target.constructor && target.constructor.name == "PlacementSupport") return { "type": "construct" }; return false; }, "specificness": 0, }, "repair": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": action.foundation ? "order_build" : "order_repair", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.builder || !targetState || !targetState.needsRepair && !targetState.foundation || !playerCheck(entState, targetState, ["Player", "Ally"])) return false; return { "possible": true, "foundation": targetState.foundation }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-repair-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.repair") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("repair", target, selection); return actionInfo.possible && { "type": "repair", "cursor": "action-repair", "target": target, "foundation": actionInfo.foundation, "firstAbleEntity": actionInfo.entity }; }, "specificness": 11, }, "gather": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "gather", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.resourceGatherRates || !targetState || !targetState.resourceSupply) return false; let resource; if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific]) resource = targetState.resourceSupply.type.specific; else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic]) resource = targetState.resourceSupply.type.generic; if (!resource) return false; return { "possible": true, "cursor": "action-gather-" + resource }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("gather", target, selection); return actionInfo.possible && { "type": "gather", "cursor": actionInfo.cursor, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 1, }, "returnresource": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "returnresource", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || !targetState.resourceDropsite) return false; let playerState = GetSimState().players[entState.player]; if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared) { if (!playerCheck(entState, targetState, ["Player", "MutualAlly"])) return false; } else if (!playerCheck(entState, targetState, ["Player"])) return false; if (!entState.resourceCarrying || !entState.resourceCarrying.length) return false; let carriedType = entState.resourceCarrying[0].type; if (targetState.resourceDropsite.types.indexOf(carriedType) == -1) return false; return { "possible": true, "cursor": "action-return-" + carriedType }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("returnresource", target, selection); return actionInfo.possible && { "type": "returnresource", "cursor": actionInfo.cursor, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 2, }, "cancel-setup-trade-route": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "cancel-setup-trade-route", "entities": selection, "target": action.target, "queued": queued }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || targetState.foundation || !entState.trader || !targetState.market || playerCheck(entState, targetState, ["Enemy"]) || !(targetState.market.land && hasClass(entState, "Organic") || targetState.market.naval && hasClass(entState, "Ship"))) return false; let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", { "trader": entState.id, "target": targetState.id }); if (!tradingDetails || !tradingDetails.type) return false; if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets) return { "possible": true, "tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.") }; return false; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("cancel-setup-trade-route", target, selection); return actionInfo.possible && { "type": "cancel-setup-trade-route", "cursor": "action-cancel-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 2, }, "setup-trade-route": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({ "type": "setup-trade-route", "entities": selection, "target": action.target, "source": null, "route": null, "queued": queued, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || targetState.foundation || !entState.trader || !targetState.market || playerCheck(entState, targetState, ["Enemy"]) || !(targetState.market.land && hasClass(entState, "Organic") || targetState.market.naval && hasClass(entState, "Ship"))) return false; let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", { "trader": entState.id, "target": targetState.id }); if (!tradingDetails) return false; let tooltip; switch (tradingDetails.type) { case "is first": tooltip = translate("Origin trade market.") + "\n"; if (tradingDetails.hasBothMarkets) tooltip += sprintf(translate("Gain: %(gain)s"), { "gain": getTradingTooltip(tradingDetails.gain) }); else return false; break; case "is second": tooltip = translate("Destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { "gain": getTradingTooltip(tradingDetails.gain) }); break; case "set first": tooltip = translate("Right-click to set as origin trade market"); break; case "set second": if (tradingDetails.gain.traderGain == 0) return { "possible": true, "tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags), "disabled": true }; tooltip = translate("Right-click to set as destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { "gain": getTradingTooltip(tradingDetails.gain) }); break; } return { "possible": true, "tooltip": tooltip }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("setup-trade-route", target, selection); if (actionInfo.disabled) return { "type": "none", "cursor": "action-setup-trade-route-disabled", "target": null, "tooltip": actionInfo.tooltip }; return actionInfo.possible && { "type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 0, }, "occupy-turret": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "occupy-turret", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.turretable || !targetState || !targetState.turretHolder || !playerCheck(entState, targetState, ["Player", "MutualAlly"])) return false; if (!targetState.turretHolder.turretPoints.find(point => !point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses))) return false; let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null); let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), { "occupied": occupiedTurrets.length, "capacity": targetState.turretHolder.turretPoints.length }); if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length) tooltip = coloredText(tooltip, "orange"); return { "possible": true, "tooltip": tooltip }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_OCCUPY_TURRET && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-occupy-turret-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.occupyturret") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("occupy-turret", target, selection); return actionInfo.possible && { "type": "occupy-turret", "cursor": "action-occupy-turret", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 21, }, "garrison": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "garrison", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.garrisonable || !targetState || !targetState.garrisonHolder || !playerCheck(entState, targetState, ["Player", "MutualAlly"])) return false; let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { "garrisoned": targetState.garrisonHolder.occupiedSlots, "capacity": targetState.garrisonHolder.capacity }); let extraCount = entState.garrisonable.size; if (entState.garrisonHolder) extraCount += entState.garrisonHolder.occupiedSlots; if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity) tooltip = coloredText(tooltip, "orange"); if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) return false; return { "possible": true, "tooltip": tooltip }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-garrison-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.garrison") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("garrison", target, selection); return actionInfo.possible && { "type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 20, }, "guard": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "guard", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || !targetState.guard || entState.id == targetState.id || !playerCheck(entState, targetState, ["Player", "Ally"]) || !entState.unitAI || !entState.unitAI.canGuard) return false; return { "possible": true }; }, "preSelectedActionCheck": function(target, selection) { return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || { "type": "none", "cursor": "action-guard-disabled", "target": null }); }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.guard") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("guard", target, selection); return actionInfo.possible && { "type": "guard", "cursor": "action-guard", "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 40, }, "collect-treasure": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({ "type": "collect-treasure", "entities": selection, "target": action.target, "queued": queued, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_collect_treasure", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.treasureCollector || !targetState || !targetState.treasure) return false; return { "possible": true, "cursor": "action-collect-treasure" }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("collect-treasure", target, selection); return actionInfo.possible && { "type": "collect-treasure", "cursor": actionInfo.cursor, "target": target, "firstAbleEntity": actionInfo.entity }; }, "specificness": 1, }, "remove-guard": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "remove-guard", "entities": selection, "target": action.target, "queued": queued, "pushFront": pushFront }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": action.firstAbleEntity }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.unitAI || !entState.unitAI.isGuarding) return false; return { "possible": true }; }, "hotkeyActionCheck": function(target, selection) { return Engine.HotkeyIsPressed("session.guard") && this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("remove-guard", target, selection); return actionInfo.possible && { "type": "remove-guard", "cursor": "action-remove-guard", "firstAbleEntity": actionInfo.entity }; }, "specificness": 41, }, "set-rallypoint": { "execute": function(target, action, selection, queued, pushFront) { // if there is a position set in the action then use this so that when setting a // rally point on an entity it is centered on that entity if (action.position) target = action.position; Engine.PostNetworkCommand({ "type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z, "data": action.data, "queued": queued }); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z, "queued": queued }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.rallyPoint) return false; // Don't allow the rally point to be set on any of the currently selected entities (used for unset) // except if the autorallypoint hotkey is pressed and the target can produce entities. if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") || - !targetState.production || - !targetState.production.entities.length)) + !targetState.trainer || + !targetState.trainer.entities.length)) for (const ent of g_Selection.toList()) if (targetState.id == ent) return false; let tooltip; let disabled = false; // default to walking there (or attack-walking if hotkey pressed) let data = { "command": "walk" }; let cursor = ""; if (isAttackMovePressed()) { let targetClasses; if (Engine.HotkeyIsPressed("session.attackmoveUnit")) targetClasses = { "attack": ["Unit"] }; else targetClasses = { "attack": ["Unit", "Structure"] }; data.command = "attack-walk"; data.targetClasses = targetClasses; cursor = "action-attack-move"; } if (Engine.HotkeyIsPressed("session.repair") && targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Player", "Ally"])) { data.command = "repair"; data.target = targetState.id; cursor = "action-repair"; } else if (targetState && targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"])) { data.command = "garrison"; data.target = targetState.id; cursor = "action-garrison"; tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { "garrisoned": targetState.garrisonHolder.occupiedSlots, "capacity": targetState.garrisonHolder.capacity }); if (targetState.garrisonHolder.occupiedSlots >= targetState.garrisonHolder.capacity) tooltip = coloredText(tooltip, "orange"); } else if (targetState && targetState.turretHolder && playerCheck(entState, targetState, ["Player", "MutualAlly"])) { data.command = "occupy-turret"; data.target = targetState.id; cursor = "action-garrison"; let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null); tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), { "occupied": occupiedTurrets.length, "capacity": targetState.turretHolder.turretPoints.length }); if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length) tooltip = coloredText(tooltip, "orange"); } else if (targetState && targetState.resourceSupply) { let resourceType = targetState.resourceSupply.type; cursor = "action-gather-" + resourceType.specific; data.command = "gather-near-position"; data.resourceType = resourceType; data.resourceTemplate = targetState.template; if (!targetState.speed) { data.command = "gather"; data.target = targetState.id; } } else if (targetState && targetState.treasure) { cursor = "action-collect-treasure"; data.command = "collect-treasure-near-position"; if (!targetState.speed) { data.command = "collect-treasure"; data.target = targetState.id; } } else if (entState.market && targetState && targetState.market && entState.id != targetState.id && (!entState.market.naval || targetState.market.naval) && !playerCheck(entState, targetState, ["Enemy"])) { // Find a trader (if any) that this structure can train. let trader; - if (entState.production && entState.production.entities.length) - for (let i = 0; i < entState.production.entities.length; ++i) - if ((trader = GetTemplateData(entState.production.entities[i]).trader)) + if (entState.trainer?.entities?.length) + for (let i = 0; i < entState.trainer.entities.length; ++i) + if ((trader = GetTemplateData(entState.trainer.entities[i]).trader)) break; let traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader }; let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData); if (gain) { data.command = "trade"; data.target = traderData.secondMarket; data.source = traderData.firstMarket; cursor = "action-setup-trade-route"; if (gain.traderGain) tooltip = translate("Right-click to establish a default route for new traders.") + "\n" + sprintf( trader ? translate("Gain: %(gain)s") : translate("Expected gain: %(gain)s"), { "gain": getTradingTooltip(gain) }); else { disabled = true; tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags); cursor = "action-setup-trade-route-disabled"; } } } else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"])) { data.command = "repair"; data.target = targetState.id; cursor = "action-repair"; } else if (targetState && playerCheck(entState, targetState, ["Enemy"])) { data.target = targetState.id; data.command = "attack"; cursor = "action-attack"; } return { "possible": true, "data": data, "position": targetState && targetState.position, "cursor": cursor, "disabled": disabled, "tooltip": tooltip }; }, "hotkeyActionCheck": function(target, selection) { // Hotkeys are checked in the actionInfo. return this.actionCheck(target, selection); }, "actionCheck": function(target, selection) { // We want commands to units take precedence. if (selection.some(ent => { let entState = GetEntityState(ent); return entState && !!entState.unitAI; })) return false; let actionInfo = getActionInfo("set-rallypoint", target, selection); if (actionInfo.disabled) return { "type": "none", "cursor": actionInfo.cursor, "target": null, "tooltip": actionInfo.tooltip }; return actionInfo.possible && { "type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position, "firstAbleEntity": actionInfo.entity }; }, "specificness": 6, }, "unset-rallypoint": { "execute": function(target, action, selection, queued, pushFront) { Engine.PostNetworkCommand({ "type": "unset-rallypoint", "entities": selection }); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState || entState.id != targetState.id || entState.unitAI || !entState.rallyPoint || !entState.rallyPoint.position) return false; return { "possible": true }; }, "actionCheck": function(target, selection) { let actionInfo = getActionInfo("unset-rallypoint", target, selection); return actionInfo.possible && { "type": "unset-rallypoint", "cursor": "action-unset-rally", "firstAbleEntity": actionInfo.entity }; }, "specificness": 11, }, // This is a "fake" action to show a failure cursor // when only uncontrollable entities are selected. "uncontrollable": { "execute": function(target, action, selection, queued) { return true; }, "actionCheck": function(target, selection) { // Only show this action if all entities are marked uncontrollable. let playerState = g_SimState.players[g_ViewedPlayer]; if (playerState && playerState.controlsAll || selection.some(ent => { let entState = GetEntityState(ent); return entState && entState.identity && entState.identity.controllable; })) return false; return { "type": "none", "cursor": "cursor-no", "tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length) }; }, "specificness": 100, }, "none": { "execute": function(target, action, selection, queued) { return true; }, "specificness": 100, }, }; var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness); /** * Info and actions for the entity commands * Currently displayed in the bottom of the central panel */ var g_EntityCommands = { "unload-all": { "getInfo": function(entStates) { let count = 0; for (let entState of entStates) { if (!entState.garrisonHolder) continue; if (allowedPlayersCheck([entState], ["Player"])) count += entState.garrisonHolder.entities.length; else for (let entity of entState.garrisonHolder.entities) if (allowedPlayersCheck([GetEntityState(entity)], ["Player"])) ++count; } if (!count) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") + translate("Unload All."), "icon": "garrison-out.png", "count": count, "enabled": true }; }, "execute": function() { unloadAll(); }, "allowedPlayers": ["Player", "Ally"] }, "unload-all-turrets": { "getInfo": function(entStates) { let count = 0; for (let entState of entStates) { if (!entState.turretHolder) continue; if (allowedPlayersCheck([entState], ["Player"])) count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity && turretPoint.ejectable).length; else for (let turretPoint of entState.turretHolder.turretPoints) if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"])) ++count; } if (!count) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") + translate("Unload Turrets."), "icon": "garrison-out.png", "count": count, "enabled": true }; }, "execute": function() { unloadAllTurrets(); }, "allowedPlayers": ["Player", "Ally"] }, "delete": { "getInfo": function(entStates) { return entStates.some(entState => !isUndeletable(entState)) ? { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.kill") + translate("Destroy the selected units or structures.") + "\n" + colorizeHotkey( translate("Use %(hotkey)s to avoid the confirmation dialog."), "session.noconfirmation" ), "icon": "kill_small.png", "enabled": true } : { // Get all delete reasons and remove duplications "tooltip": entStates.map(entState => isUndeletable(entState)) .filter((reason, pos, self) => self.indexOf(reason) == pos && reason ).join("\n"), "icon": "kill_small_disabled.png", "enabled": false }; }, "execute": function(entStates) { let entityIDs = entStates.reduce( (ids, entState) => { if (!isUndeletable(entState)) ids.push(entState.id); return ids; }, []); if (!entityIDs.length) return; let deleteSelection = () => Engine.PostNetworkCommand({ "type": "delete-entities", "entities": entityIDs }); if (Engine.HotkeyIsPressed("session.noconfirmation")) deleteSelection(); else (new DeleteSelectionConfirmation(deleteSelection)).display(); }, "allowedPlayers": ["Player"] }, "stop": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") + translate("Abort the current order."), "icon": "stop.png", "enabled": true }; }, "execute": function(entStates) { if (entStates.length) stopUnits(entStates.map(entState => entState.id)); }, "allowedPlayers": ["Player"] }, "call-to-arms": { "getInfo": function(entStates) { const classes = ["Soldier", "Warship", "Siege", "Healer"]; if (entStates.every(entState => !MatchesClassList(entState.identity.classes, classes))) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.calltoarms") + translate("Send the selected units on attack move to the specified location after dropping resources."), "icon": "call-to-arms.png", "enabled": true }; }, "execute": function(entStates) { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_CALLTOARMS; }, "allowedPlayers": ["Player"] }, "garrison": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.garrisonable || entState.garrisonable.holder != INVALID_ENTITY)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") + translate("Order the selected units to garrison in a structure or unit."), "icon": "garrison.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; }, "allowedPlayers": ["Player"] }, "occupy-turret": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.turretable || entState.turretable.holder != INVALID_ENTITY)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.occupyturret") + translate("Order the selected units to occupy a turret point."), "icon": "occupy-turret.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_OCCUPY_TURRET; }, "allowedPlayers": ["Player"] }, "leave-turret": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.turretable || entState.turretable.holder == INVALID_ENTITY || !entState.turretable.ejectable)) return false; return { "tooltip": translate("Unload"), "icon": "leave-turret.png", "enabled": true }; }, "execute": function(entStates) { leaveTurretPoints(); }, "allowedPlayers": ["Player"] }, "repair": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.builder)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") + translate("Order the selected units to repair a structure, ship, or siege engine."), "icon": "repair.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; }, "allowedPlayers": ["Player"] }, "focus-rally": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.rallyPoint)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") + translate("Focus on Rally Point."), "icon": "focus-rally.png", "enabled": true }; }, "execute": function(entStates) { // TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first let focusTarget; for (let entState of entStates) if (entState.rallyPoint && entState.rallyPoint.position) { focusTarget = entState.rallyPoint.position; break; } if (!focusTarget) for (let entState of entStates) if (entState.position) { focusTarget = entState.position; break; } if (focusTarget) Engine.CameraMoveTo(focusTarget.x, focusTarget.z); }, "allowedPlayers": ["Player", "Observer"] }, "back-to-work": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") + translate("Back to Work"), "icon": "back-to-work.png", "enabled": true }; }, "execute": function() { backToWork(); }, "allowedPlayers": ["Player"] }, "add-guard": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") + translate("Order the selected units to guard a structure or unit."), "icon": "add-guard.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GUARD; }, "allowedPlayers": ["Player"] }, "remove-guard": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding)) return false; return { "tooltip": translate("Remove guard"), "icon": "remove-guard.png", "enabled": true }; }, "execute": function() { removeGuard(); }, "allowedPlayers": ["Player"] }, "select-trading-goods": { "getInfo": function(entStates) { if (entStates.every(entState => !entState.market)) return false; return { "tooltip": translate("Barter & Trade"), "icon": "economics.png", "enabled": true }; }, "execute": function() { g_TradeDialog.toggle(); }, "allowedPlayers": ["Player"] }, "patrol": { "getInfo": function(entStates) { if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") + translate("Patrol") + "\n" + translate("Attack all encountered enemy units while avoiding structures."), "icon": "patrol.png", "enabled": true }; }, "execute": function() { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_PATROL; }, "allowedPlayers": ["Player"] }, "share-dropsite": { "getInfo": function(entStates) { let sharableEntities = entStates.filter( entState => entState.resourceDropsite && entState.resourceDropsite.sharable); if (!sharableEntities.length) return false; // Returns if none of the entities belong to a player with a mutual ally. if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some( (isAlly, playerId) => isAlly && playerId != entState.player))) return false; return sharableEntities.some(entState => !entState.resourceDropsite.shared) ? { "tooltip": translate("Press to allow allies to use this dropsite"), "icon": "locked_small.png", "enabled": true } : { "tooltip": translate("Press to prevent allies from using this dropsite"), "icon": "unlocked_small.png", "enabled": true }; }, "execute": function(entStates) { let sharableEntities = entStates.filter( entState => entState.resourceDropsite && entState.resourceDropsite.sharable); if (sharableEntities) Engine.PostNetworkCommand({ "type": "set-dropsite-sharing", "entities": sharableEntities.map(entState => entState.id), "shared": sharableEntities.some(entState => !entState.resourceDropsite.shared) }); }, "allowedPlayers": ["Player"] }, "is-dropsite-shared": { "getInfo": function(entStates) { let shareableEntities = entStates.filter( entState => entState.resourceDropsite && entState.resourceDropsite.sharable); if (!shareableEntities.length) return false; let player = Engine.GetPlayerID(); let simState = GetSimState(); if (!g_IsObserver && !simState.players[player].hasSharedDropsites || shareableEntities.every(entState => controlsPlayer(entState.player))) return false; if (!shareableEntities.every(entState => entState.resourceDropsite.shared)) return { "tooltip": translate("The use of this dropsite is prohibited"), "icon": "locked_small.png", "enabled": false }; return { "tooltip": g_IsObserver ? translate("Allies are allowed to use this dropsite.") : translate("You are allowed to use this dropsite"), "icon": "unlocked_small.png", "enabled": false }; }, "execute": function(entState) { // This command button is always disabled. }, "allowedPlayers": ["Ally", "Observer"] }, "autoqueue-on": { "getInfo": function(entStates) { - if (entStates.every(entState => !entState.production || !entState.production.entities.length || entState.production.autoqueue)) + if (entStates.every(entState => !entState.trainer || !entState.trainer.entities.length || entState.production.autoqueue)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") + translate("Activate auto-queue for selected structures."), "icon": "autoqueue-on.png", "enabled": true }; }, "execute": function(entStates) { if (entStates.length) turnAutoQueueOn(); }, "allowedPlayers": ["Player"] }, "autoqueue-off": { "getInfo": function(entStates) { - if (entStates.every(entState => !entState.production || !entState.production.entities.length || !entState.production.autoqueue)) + if (entStates.every(entState => !entState.trainer || !entState.trainer.entities.length || !entState.production.autoqueue)) return false; return { "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") + translate("Deactivate auto-queue for selected structures."), "icon": "autoqueue-off.png", "enabled": true }; }, "execute": function(entStates) { if (entStates.length) turnAutoQueueOff(); }, "allowedPlayers": ["Player"] }, }; function playerCheck(entState, targetState, validPlayers) { let playerState = GetSimState().players[entState.player]; for (let player of validPlayers) if (player == "Gaia" && targetState.player == 0 || player == "Player" && targetState.player == entState.player || playerState["is" + player] && playerState["is" + player][targetState.player]) return true; return false; } /** * Checks whether the entities have the right diplomatic status * with respect to the currently active player. * Also "Observer" can be used. * * @param {Object[]} entStates - An array containing the entity states to check. * @param {string[]} validPlayers - An array containing the diplomatic statuses. * * @return {boolean} - Whether the currently active player is allowed. */ function allowedPlayersCheck(entStates, validPlayers) { // Assume we can only select entities from one player, // or it does not matter (e.g. observer). let targetState = entStates[0]; let playerState = GetSimState().players[Engine.GetPlayerID()]; return validPlayers.some(player => player == "Observer" && g_IsObserver || player == "Player" && controlsPlayer(targetState.player) || playerState && playerState["is" + player] && playerState["is" + player][targetState.player]); } function hasClass(entState, className) { // note: use the functions in globalscripts/Templates.js for more versatile matching return entState.identity && entState.identity.classes.indexOf(className) != -1; } /** * Keep in sync with Commands.js. */ function isUndeletable(entState) { let playerState = g_SimState.players[entState.player]; if (playerState && playerState.controlsAll) return false; if (entState.resourceSupply && entState.resourceSupply.killBeforeGather) return translate("The entity has to be killed before it can be gathered from"); if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2) return translate("You cannot destroy this entity as you own less than half the capture points"); if (!entState.identity.canDelete) return translate("This entity is undeletable"); return false; } function DrawTargetMarker(target) { Engine.GuiInterfaceCall("AddTargetMarker", { "template": g_TargetMarker.move, "x": target.x, "z": target.z }); } function displayFlare(target, playerID) { Engine.GuiInterfaceCall("AddTargetMarker", { "template": g_TargetMarker.map_flare, "x": target.x, "z": target.z, "owner": playerID }); g_MiniMapPanel.flare(target, playerID); } function getCommandInfo(command, entStates) { return entStates && g_EntityCommands[command] && allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) && g_EntityCommands[command].getInfo(entStates); } function getActionInfo(action, target, selection) { if (!selection || !selection.length || !GetEntityState(selection[0])) return { "possible": false }; // Look at the first targeted entity // (TODO: maybe we eventually want to look at more, and be more context-sensitive? // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse) let targetState = GetEntityState(target); let simState = GetSimState(); let playerState = g_SimState.players[g_ViewedPlayer]; // Check if any entities in the selection can do some of the available actions. for (let entityID of selection) { let entState = GetEntityState(entityID); if (!entState) continue; if (playerState && !playerState.controlsAll && !entState.identity.controllable) continue; if (g_UnitActions[action] && g_UnitActions[action].getActionInfo) { let r = g_UnitActions[action].getActionInfo(entState, targetState, simState); if (r && r.possible) { r.entity = entityID; return r; } } } return { "possible": false }; } Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 26000) @@ -1,234 +1,234 @@ // The number of currently visible buttons (used to optimise showing/hiding) var g_unitPanelButtons = { "Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Research": 0, "Alert": 0, "Barter": 0, "Construction": 0, "Command": 0, "Stance": 0, "Gate": 0, "Pack": 0, "Upgrade": 0 }; /** * Set the position of a panel object according to the index, * from left to right, from top to bottom. * Will wrap around to subsequent rows if the index * is larger than rowLength. */ function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin = 1) { var size = object.size; // horizontal position var oWidth = size.right - size.left; var hIndex = index % rowLength; size.left = hIndex * (oWidth + vMargin); size.right = size.left + oWidth; // vertical position var oHeight = size.bottom - size.top; var vIndex = Math.floor(index / rowLength); size.top = vIndex * (oHeight + hMargin); size.bottom = size.top + oHeight; object.size = size; } /** * Helper function for updateUnitCommands; sets up "unit panels" * (i.e. panels with rows of icons) for the currently selected unit. * * @param guiName Short identifier string of this panel. See g_SelectionPanels. * @param unitEntStates Entity states of the selected units * @param playerState Player state */ function setupUnitPanel(guiName, unitEntStates, playerState) { if (!g_SelectionPanels[guiName]) { error("unknown guiName used '" + guiName + "'"); return; } let items = g_SelectionPanels[guiName].getItems(unitEntStates); if (!items || !items.length) return; let numberOfItems = Math.min(items.length, g_SelectionPanels[guiName].getMaxNumberOfItems()); let rowLength = g_SelectionPanels[guiName].rowLength || 8; if (g_SelectionPanels[guiName].resizePanel) g_SelectionPanels[guiName].resizePanel(numberOfItems, rowLength); for (let i = 0; i < numberOfItems; ++i) { let data = { "i": i, "item": items[i], "playerState": playerState, "player": unitEntStates[0].player, "unitEntStates": unitEntStates, "rowLength": rowLength, "numberOfItems": numberOfItems, // depending on the XML, some of the GUI objects may be undefined "button": Engine.GetGUIObjectByName("unit" + guiName + "Button[" + i + "]"), "icon": Engine.GetGUIObjectByName("unit" + guiName + "Icon[" + i + "]"), "guiSelection": Engine.GetGUIObjectByName("unit" + guiName + "Selection[" + i + "]"), "countDisplay": Engine.GetGUIObjectByName("unit" + guiName + "Count[" + i + "]") }; if (data.button) { data.button.hidden = false; data.button.enabled = true; data.button.tooltip = ""; data.button.caption = ""; } if (g_SelectionPanels[guiName].setupButton && !g_SelectionPanels[guiName].setupButton(data)) continue; // TODO: we should require all entities to have icons, so this case never occurs if (data.icon && !data.icon.sprite) data.icon.sprite = "BackgroundBlack"; } // Hide any buttons we're no longer using for (let i = numberOfItems; i < g_unitPanelButtons[guiName]; ++i) if (g_SelectionPanels[guiName].hideItem) g_SelectionPanels[guiName].hideItem(i, rowLength); else Engine.GetGUIObjectByName("unit" + guiName + "Button[" + i + "]").hidden = true; g_unitPanelButtons[guiName] = numberOfItems; g_SelectionPanels[guiName].used = true; } /** * Updates the selection panels where buttons are supposed to * depend on the context. * Runs in the main session loop via updateSelectionDetails(). * Delegates to setupUnitPanel to set up individual subpanels, * appropriately activated depending on the selected unit's state. * * @param entStates Entity states of the selected units * @param supplementalDetailsPanel Reference to the * "supplementalSelectionDetails" GUI Object * @param commandsPanel Reference to the "commandsPanel" GUI Object */ function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel) { for (let panel in g_SelectionPanels) g_SelectionPanels[panel].used = false; // Get player state to check some constraints // e.g. presence of a hero or build limits. let playerStates = GetSimState().players; let playerState = playerStates[Engine.GetPlayerID()]; setupUnitPanel("Selection", entStates, playerStates[entStates[0].player]); // Command panel always shown for it can contain commands // for which the entity does not need to be owned. setupUnitPanel("Command", entStates, playerState); if (g_IsObserver || entStates.every(entState => controlsPlayer(entState.player) && (!entState.identity || entState.identity.controllable)) || playerState.controlsAll) { for (let guiName of g_PanelsOrder) { if (g_SelectionPanels[guiName].conflictsWith && g_SelectionPanels[guiName].conflictsWith.some(p => g_SelectionPanels[p].used)) continue; setupUnitPanel(guiName, entStates, playerStates[entStates[0].player]); } supplementalDetailsPanel.hidden = false; commandsPanel.hidden = false; } else if (playerState.isMutualAlly[entStates[0].player]) { // TODO if there's a second panel needed for a different player // we should consider adding the players list to g_SelectionPanels setupUnitPanel("Garrison", entStates, playerState); supplementalDetailsPanel.hidden = !g_SelectionPanels.Garrison.used; commandsPanel.hidden = true; } else { supplementalDetailsPanel.hidden = true; commandsPanel.hidden = true; } // Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time) for (let panelName in g_SelectionPanels) Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = !g_SelectionPanels[panelName].used; } // Force hide commands panels function hideUnitCommands() { for (var panelName in g_SelectionPanels) Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = true; } // Get all of the available entities which can be trained by the selected entities function getAllTrainableEntities(selection) { let trainableEnts = []; // Get all buildable and trainable entities for (let ent of selection) { let state = GetEntityState(ent); - if (state && state.production && state.production.entities.length) - trainableEnts = trainableEnts.concat(state.production.entities); + if (state?.trainer?.entities?.length) + trainableEnts = trainableEnts.concat(state.trainer.entities); } // Remove duplicates removeDupes(trainableEnts); return trainableEnts; } function getAllTrainableEntitiesFromSelection() { if (!g_allTrainableEntities) g_allTrainableEntities = getAllTrainableEntities(g_Selection.toList()); return g_allTrainableEntities; } // Get all of the available entities which can be built by the selected entities function getAllBuildableEntities(selection) { return Engine.GuiInterfaceCall("GetAllBuildableEntities", { "entities": selection }); } function getAllBuildableEntitiesFromSelection() { if (!g_allBuildableEntities) g_allBuildableEntities = getAllBuildableEntities(g_Selection.toList()); return g_allBuildableEntities; } function getNumberOfRightPanelButtons() { var sum = 0; for (let prop of ["Construction", "Training", "Pack", "Gate", "Upgrade"]) if (g_SelectionPanels[prop].used) sum += g_unitPanelButtons[prop]; return sum; } Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 26000) @@ -1,1027 +1,1026 @@ var API3 = function(m) { // defines a template. m.Template = m.Class({ "_init": function(sharedAI, templateName, template) { this._templateName = templateName; this._template = template; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; this._tpCache = new Map(); }, // Helper function to return a template value, adjusting for tech. "get": function(string) { if (this._entityModif && this._entityModif.has(string)) return this._entityModif.get(string); else if (this._templateModif) { let owner = this._entity ? this._entity.owner : PlayerID; if (this._templateModif[owner] && this._templateModif[owner].has(string)) return this._templateModif[owner].get(string); } if (!this._tpCache.has(string)) { let value = this._template; let args = string.split("/"); for (let arg of args) { value = value[arg]; if (value == undefined) break; } this._tpCache.set(string, value); } return this._tpCache.get(string); }, "templateName": function() { return this._templateName; }, "genericName": function() { return this.get("Identity/GenericName"); }, "civ": function() { return this.get("Identity/Civ"); }, "matchLimit": function() { if (!this.get("TrainingRestrictions")) return undefined; return this.get("TrainingRestrictions/MatchLimit"); }, "classes": function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, "hasClass": function(name) { if (!this._classes) this._classes = this.classes(); return this._classes && this._classes.indexOf(name) != -1; }, "hasClasses": function(array) { if (!this._classes) this._classes = this.classes(); return this._classes && MatchesClassList(this._classes, array); }, "requiredTech": function() { return this.get("Identity/RequiredTechnology"); }, "available": function(gameState) { let techRequired = this.requiredTech(); if (!techRequired) return true; return gameState.isResearched(techRequired); }, // specifically "phase": function() { let techRequired = this.requiredTech(); if (!techRequired) return 0; if (techRequired == "phase_village") return 1; if (techRequired == "phase_town") return 2; if (techRequired == "phase_city") return 3; if (techRequired.startsWith("phase_")) return 4; return 0; }, "cost": function(productionQueue) { if (!this.get("Cost")) return {}; let ret = {}; for (let type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, "costSum": function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return 0; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { - return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1); + return +(this.get("Researcher/TechCostMultiplier/"+type) || 1); }, /** * Returns { "max": max, "min": min } or undefined if no obstruction. * max: radius of the outer circle surrounding this entity's obstruction shape * min: radius of the inner circle */ "obstructionRadius": function() { if (!this.get("Obstruction")) return undefined; if (this.get("Obstruction/Static")) { let w = +this.get("Obstruction/Static/@width"); let h = +this.get("Obstruction/Static/@depth"); return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 }; } if (this.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); return { "max": r, "min": r }; } let right = this.get("Obstruction/Obstructions/Right"); let left = this.get("Obstruction/Obstructions/Left"); if (left && right) { let w = +right["@x"] + right["@width"] / 2 - left["@x"] + left["@width"] / 2; let h = Math.max(+right["@z"] + right["@depth"] / 2, +left["@z"] + left["@depth"] / 2) - Math.min(+right["@z"] - right["@depth"] / 2, +left["@z"] - left["@depth"] / 2); return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 }; } return { "max": 0, "min": 0 }; // Units have currently no obstructions }, /** * Returns the radius of a circle surrounding this entity's footprint. */ "footprintRadius": function() { if (!this.get("Footprint")) return undefined; if (this.get("Footprint/Square")) { let w = +this.get("Footprint/Square/@width"); let h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w * w + h * h) / 2; } if (this.get("Footprint/Circle")) return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, "maxHitpoints": function() { return +(this.get("Health/Max") || 0); }, "isHealable": function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, "isRepairable": function() { return this.get("Repairable") !== undefined; }, "getPopulationBonus": function() { if (!this.get("Population")) return 0; return +this.get("Population/Bonus"); }, "resistanceStrengths": function() { let resistanceTypes = this.get("Resistance"); if (!resistanceTypes || !resistanceTypes.Entity) return undefined; let resistance = {}; if (resistanceTypes.Entity.Capture) resistance.Capture = +this.get("Resistance/Entity/Capture"); if (resistanceTypes.Entity.Damage) { resistance.Damage = {}; for (let damageType in resistanceTypes.Entity.Damage) resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType); } // ToDo: Resistance to StatusEffects. return resistance; }, "attackTypes": function() { let attack = this.get("Attack"); if (!attack) return undefined; let ret = []; for (let type in attack) ret.push(type); return ret; }, "attackRange": function(type) { if (!this.get("Attack/" + type)) return undefined; return { "max": +this.get("Attack/" + type +"/MaxRange"), "min": +(this.get("Attack/" + type +"/MinRange") || 0) }; }, "attackStrengths": function(type) { let attackDamageTypes = this.get("Attack/" + type + "/Damage"); if (!attackDamageTypes) return undefined; let damage = {}; for (let damageType in attackDamageTypes) damage[damageType] = +attackDamageTypes[damageType]; return damage; }, "captureStrength": function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Capture") || 0; }, "attackTimes": function(type) { if (!this.get("Attack/" + type)) return undefined; return { "prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0), "repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses- , multiplier], … ]. "getCounteredClasses": function() { let attack = this.get("Attack"); if (!attack) return undefined; let Classes = []; for (let type in attack) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]); } } return Classes; }, // returns true if the entity counters the target entity. // TODO: refine using the multiplier "counters": function(target) { let attack = this.get("Attack"); if (!attack) return false; let mcounter = []; for (let type in attack) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) mcounter.concat(bonusClasses.split(" ")); } } return target.hasClasses(mcounter); }, // returns, if it exists, the multiplier from each attack against a given class "getMultiplierAgainst": function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; let bonuses = this.get("Attack/" + type + "/Bonuses"); if (bonuses) { for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (!bonusClasses) continue; for (let bcl of bonusClasses.split(" ")) if (bcl == againstClass) return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier"); } } return 1; }, "buildableEntities": function(civ) { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "trainableEntities": function(civ) { - let templates = this.get("ProductionQueue/Entities/_string"); + const templates = this.get("Trainer/Entities/_string"); if (!templates) return undefined; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "researchableTechs": function(gameState, civ) { - let templates = this.get("ProductionQueue/Technologies/_string"); + const templates = this.get("Researcher/Technologies/_string"); if (!templates) return undefined; let techs = templates.split(/\s+/); for (let i = 0; i < techs.length; ++i) { let tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; let civTech = tech.replace("{civ}", civ); techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } return techs; }, "resourceSupplyType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); return { "generic": type, "specific": subtype }; }, "getResourceType": function() { if (!this.get("ResourceSupply")) return undefined; return this.get("ResourceSupply/Type").split('.')[0]; }, "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }, "resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); }, "maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }, "resourceGatherRates": function() { if (!this.get("ResourceGatherer")) return undefined; let ret = {}; let baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (let r in this.get("ResourceGatherer/Rates")) ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed; return ret; }, "resourceDropsiteTypes": function() { if (!this.get("ResourceDropsite")) return undefined; let types = this.get("ResourceDropsite/Types"); return types ? types.split(/\s+/) : []; }, "isResourceDropsite": function(resourceType) { const types = this.resourceDropsiteTypes(); return types && (!resourceType || types.indexOf(resourceType) !== -1); }, "isTreasure": function() { return this.get("Treasure") !== undefined; }, "treasureResources": function() { if (!this.get("Treasure")) return undefined; let ret = {}; for (let r in this.get("Treasure/Resources")) ret[r] = +this.get("Treasure/Resources/" + r); return ret; }, "garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); }, "garrisonMax": function() { return this.get("GarrisonHolder/Max"); }, "garrisonSize": function() { return this.get("Garrisonable/Size"); }, "garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); }, "getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); }, "getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, "getGarrisonArrowClasses": function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, "buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); }, "promotion": function() { return this.get("Promotion/Entity"); }, "isPackable": function() { return this.get("Pack") != undefined; }, "isHuntable": function() { // Do not hunt retaliating animals (dead animals can be used). // Assume entities which can attack, will attack. return this.get("ResourceSupply/KillBeforeGather") && (!this.get("Health") || !this.get("Attack")); }, "walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); }, "trainingCategory": function() { return this.get("TrainingRestrictions/Category"); }, - "buildTime": function(productionQueue) { + "buildTime": function(researcher) { let time = +this.get("Cost/BuildTime"); - if (productionQueue) - time *= productionQueue.techCostMultiplier("time"); + if (researcher) + time *= researcher.techCostMultiplier("time"); return time; }, "buildCategory": function() { return this.get("BuildRestrictions/Category"); }, "buildDistance": function() { let distance = this.get("BuildRestrictions/Distance"); if (!distance) return undefined; let ret = {}; for (let key in distance) ret[key] = this.get("BuildRestrictions/Distance/" + key); return ret; }, "buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); }, "buildTerritories": function() { if (!this.get("BuildRestrictions")) return undefined; let territory = this.get("BuildRestrictions/Territory"); return !territory ? undefined : territory.split(/\s+/); }, "hasBuildTerritory": function(territory) { let territories = this.buildTerritories(); return territories && territories.indexOf(territory) != -1; }, "hasTerritoryInfluence": function() { return this.get("TerritoryInfluence") !== undefined; }, "hasDefensiveFire": function() { if (!this.get("Attack/Ranged")) return false; return this.getDefaultArrow() || this.getArrowMultiplier(); }, "territoryInfluenceRadius": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); return -1; }, "territoryInfluenceWeight": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); return -1; }, "territoryDecayRate": function() { return +(this.get("TerritoryDecay/DecayRate") || 0); }, "defaultRegenRate": function() { return +(this.get("Capturable/RegenRate") || 0); }, "garrisonRegenRate": function() { return +(this.get("Capturable/GarrisonRegenRate") || 0); }, "visionRange": function() { return +this.get("Vision/Range"); }, "gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); }, "isBuilder": function() { return this.get("Builder") !== undefined; }, "isGatherer": function() { return this.get("ResourceGatherer") !== undefined; }, "canGather": function(type) { let gatherRates = this.get("ResourceGatherer/Rates"); if (!gatherRates) return false; for (let r in gatherRates) if (r.split('.')[0] === type) return true; return false; }, "isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; }, "isTurretHolder": function() { return this.get("TurretHolder") !== undefined; }, /** * returns true if the tempalte can capture the given target entity * if no target is given, returns true if the template has the Capture attack */ "canCapture": function(target) { if (!this.get("Attack/Capture")) return false; if (!target) return true; if (!target.get("Capturable")) return false; let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string"); return !restrictedClasses || !target.hasClasses(restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return "Garrisonable" in this._template; }, "canOccupyTurret": function() { return "Turretable" in this._template; }, "isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; }, }); // defines an entity, with a super Template. // also redefines several of the template functions where the only change is applying aura and tech modifications. m.Entity = m.Class({ "_super": m.Template, "_init": function(sharedAI, entity) { this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template)); this._entity = entity; this._ai = sharedAI; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; // save a reference to the entity tech/aura modifications if (!sharedAI._entitiesModifications.has(entity.id)) sharedAI._entitiesModifications.set(entity.id, new Map()); this._entityModif = sharedAI._entitiesModifications.get(entity.id); }, "toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, "id": function() { return this._entity.id; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data should not be shared with any other AI scripts.) */ "getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); }, /** * Sets extra data to be associated with this entity. */ "setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); }, "deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; }, "deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); }, "position": function() { return this._entity.position; }, "angle": function() { return this._entity.angle; }, "isIdle": function() { return this._entity.idle; }, "getStance": function() { return this._entity.stance; }, "unitAIState": function() { return this._entity.unitAIState; }, "unitAIOrderData": function() { return this._entity.unitAIOrderData; }, "hitpoints": function() { return this._entity.hitpoints; }, "isHurt": function() { return this.hitpoints() < this.maxHitpoints(); }, "healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); }, "needsHeal": function() { return this.isHurt() && this.isHealable(); }, "needsRepair": function() { return this.isHurt() && this.isRepairable(); }, "decaying": function() { return this._entity.decaying; }, "capturePoints": function() {return this._entity.capturePoints; }, "isInvulnerable": function() { return this._entity.invulnerability || false; }, "isSharedDropsite": function() { return this._entity.sharedDropsite === true; }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ "trainingQueue": function() { return this._entity.trainingQueue; }, "trainingQueueTime": function() { let queue = this._entity.trainingQueue; if (!queue) return undefined; let time = 0; for (let item of queue) time += item.timeRemaining; return time / 1000; }, "foundationProgress": function() { return this._entity.foundationProgress; }, "getBuilders": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return []; return this._entity.foundationBuilders; }, "getBuildersNb": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return 0; return this._entity.foundationBuilders.length; }, "owner": function() { return this._entity.owner; }, "isOwn": function(player) { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === player; }, "resourceSupplyAmount": function() { return this._entity.resourceSupplyAmount; }, "resourceSupplyNumGatherers": function() { return this._entity.resourceSupplyNumGatherers; }, "isFull": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, "resourceCarrying": function() { return this._entity.resourceCarrying; }, "currentGatherRate": function() { // returns the gather rate for the current target if applicable. if (!this.get("ResourceGatherer")) return undefined; if (this.unitAIOrderData().length && this.unitAIState().split(".")[1] == "GATHER") { let res; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[0].target); else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[1].target); if (!res) return 0; let type = res.resourceSupplyType(); if (!type) return 0; let tstring = type.generic + "." + type.specific; let rate = +this.get("ResourceGatherer/BaseSpeed"); rate *= +this.get("ResourceGatherer/Rates/" +tstring); if (rate) return rate; return 0; } return undefined; }, "garrisonHolderID": function() { return this._entity.garrisonHolderID; }, "garrisoned": function() { return this._entity.garrisoned; }, "garrisonedSlots": function() { let count = 0; if (this._entity.garrisoned) for (let ent of this._entity.garrisoned) count += +this._ai._entities.get(ent).garrisonSize(); return count; }, "canGarrisonInside": function() { return this.garrisonedSlots() < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { let attack = this.get("Attack"); if (!attack) return false; for (let type in attack) { if (type == "Slaughter") continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses)) return true; } return false; }, /** * Derived from Attack.js' similary named function. * @return {boolean} - Whether an entity can attack a given target. */ "canAttackTarget": function(target, allowCapture) { let attackTypes = this.get("Attack"); if (!attackTypes) return false; let canCapture = allowCapture && this.canCapture(target); let health = target.get("Health"); if (!health) return canCapture; for (let type in attackTypes) { if (type == "Capture" ? !canCapture : target.isInvulnerable()) continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !target.hasClasses(restrictedClasses)) return true; } return false; }, "move": function(x, z, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront }); return this; }, "moveToRange": function(x, z, min, max, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront }); return this; }, "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront }); return this; }, // violent, aggressive, defensive, passive, standground "setStance": function(stance) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance}); return this; }, "stopMoving": function() { Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false }); }, "unload": function(id) { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] }); return this; }, // Unloads all owned units, don't unload allies "unloadAll": function() { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] }); return this; }, "garrison": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "occupy-turret": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "attack": function(unitId, allowCapture = true, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront }); return this; }, "collectTreasure": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "collect-treasure", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, // moveApart from a point in the opposite direction with a distance dist "moveApart": function(point, dist) { if (this.position() !== undefined) { let direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; let norm = m.VectorDistance(point, this.position()); if (norm === 0) direction = [1, 0]; else { direction[0] /= norm; direction[1] /= norm; } Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false, "pushFront": false }); } return this; }, // Flees from a unit in the opposite direction. "flee": function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0], this.position()[1] - unitToFleeFrom.position()[1]]; let dist = m.VectorDistance(unitToFleeFrom.position(), this.position()); FleeDirection[0] = 40 * FleeDirection[0] / dist; FleeDirection[1] = 40 * FleeDirection[1] / dist; Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false, "pushFront": false }); } return this; }, "gather": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "repair": function(target, autocontinue = false, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront }); return this; }, "returnResources": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "destroy": function() { Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] }); return this; }, "barter": function(buyType, sellType, amount) { Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount }); return this; }, "tradeRoute": function(target, source) { Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false }); return this; }, "setRallyPoint": function(target, command) { let data = { "command": command, "target": target.id() }; Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data }); return this; }, "unsetRallyPoint": function() { Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] }); return this; }, - "train": function(civ, type, count, metadata, promotedTypes, pushFront = false) + "train": function(civ, type, count, metadata, pushFront = false) { let trainable = this.trainableEntities(civ); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) == -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } Engine.PostCommand(PlayerID, { "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata, - "promoted": promotedTypes, "pushFront": pushFront }); return this; }, "construct": function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting Engine.PostCommand(PlayerID, { "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false, "pushFront": false, "metadata": metadata // can be undefined }); return this; }, "research": function(template, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template, "pushFront": pushFront }); return this; }, "stopProduction": function(id) { Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id }); return this; }, "stopAllProduction": function(percentToStopAt) { let queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (let item of queue) if (item.progress < percentToStopAt) Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id }); return this; }, "guard": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "removeGuard": function() { Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] }); return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 26000) @@ -1,138 +1,138 @@ LoadModificationTemplates(); var API3 = function(m) { /** Wrapper around a technology template */ m.Technology = function(templateName) { this._templateName = templateName; let template = TechnologyTemplates.Get(templateName); // check if this is one of two paired technologies. this._isPair = template.pair !== undefined; if (this._isPair) { let pairTech = TechnologyTemplates.Get(template.pair); this._pairedWith = pairTech.top == templateName ? pairTech.bottom : pairTech.top; } // check if it only defines a pair: this._definesPair = template.top !== undefined; this._template = template; }; /** returns generic, or specific if civ provided. */ m.Technology.prototype.name = function(civ) { if (civ === undefined) return this._template.genericName; if (this._template.specificName === undefined || this._template.specificName[civ] === undefined) return undefined; return this._template.specificName[civ]; }; m.Technology.prototype.pairDef = function() { return this._definesPair; }; /** in case this defines a pair only, returns the two paired technologies. */ m.Technology.prototype.getPairedTechs = function() { if (!this._definesPair) return undefined; return [ new m.Technology(this._template.top), new m.Technology(this._template.bottom) ]; }; m.Technology.prototype.pair = function() { if (!this._isPair) return undefined; return this._template.pair; }; m.Technology.prototype.pairedWith = function() { if (!this._isPair) return undefined; return this._pairedWith; }; -m.Technology.prototype.cost = function(productionQueue) +m.Technology.prototype.cost = function(researcher) { if (!this._template.cost) return undefined; let cost = {}; for (let type in this._template.cost) { cost[type] = +this._template.cost[type]; - if (productionQueue) - cost[type] *= productionQueue.techCostMultiplier(type); + if (researcher) + cost[type] *= researcher.techCostMultiplier(type); } return cost; }; -m.Technology.prototype.costSum = function(productionQueue) +m.Technology.prototype.costSum = function(researcher) { - let cost = this.cost(productionQueue); + const cost = this.cost(researcher); if (!cost) return 0; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }; m.Technology.prototype.researchTime = function() { return this._template.researchTime || 0; }; m.Technology.prototype.requirements = function(civ) { return DeriveTechnologyRequirements(this._template, civ); }; m.Technology.prototype.autoResearch = function() { if (!this._template.autoResearch) return undefined; return this._template.autoResearch; }; m.Technology.prototype.supersedes = function() { if (!this._template.supersedes) return undefined; return this._template.supersedes; }; m.Technology.prototype.modifications = function() { if (!this._template.modifications) return undefined; return this._template.modifications; }; m.Technology.prototype.affects = function() { if (!this._template.affects) return undefined; return this._template.affects; }; m.Technology.prototype.isAffected = function(classes) { return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect)); }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanTraining.js (revision 26000) @@ -1,187 +1,157 @@ PETRA.TrainingPlan = function(gameState, type, metadata, number = 1, maxMerge = 5) { if (!PETRA.QueuePlan.call(this, gameState, type, metadata)) { API3.warn(" Plan training " + type + " canceled"); return false; } // Refine the estimated cost and add pop cost let trainers = this.getBestTrainers(gameState); let trainer = trainers ? trainers[0] : undefined; this.cost = new API3.Resources(this.template.cost(trainer), +this.template._template.Cost.Population); this.category = "unit"; this.number = number; this.maxMerge = maxMerge; return true; }; PETRA.TrainingPlan.prototype = Object.create(PETRA.QueuePlan.prototype); PETRA.TrainingPlan.prototype.canStart = function(gameState) { this.trainers = this.getBestTrainers(gameState); if (!this.trainers) return false; this.cost = new API3.Resources(this.template.cost(this.trainers[0]), +this.template._template.Cost.Population); return true; }; PETRA.TrainingPlan.prototype.getBestTrainers = function(gameState) { if (this.metadata && this.metadata.trainer) { let trainer = gameState.getEntityById(this.metadata.trainer); if (trainer) return [trainer]; } let allTrainers = gameState.findTrainers(this.type); if (this.metadata && this.metadata.sea) allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "sea", this.metadata.sea)); if (this.metadata && this.metadata.base) allTrainers = allTrainers.filter(API3.Filters.byMetadata(PlayerID, "base", this.metadata.base)); if (!allTrainers || !allTrainers.hasEntities()) return undefined; // Keep only trainers with smallest cost let costMin = Math.min(); let trainers; for (let ent of allTrainers.values()) { let cost = this.template.costSum(ent); if (cost === costMin) trainers.push(ent); else if (cost < costMin) { costMin = cost; trainers = [ent]; } } return trainers; }; PETRA.TrainingPlan.prototype.start = function(gameState) { if (this.metadata && this.metadata.trainer) { let metadata = {}; for (let key in this.metadata) if (key !== "trainer") metadata[key] = this.metadata[key]; this.metadata = metadata; } if (this.trainers.length > 1) { let wantedIndex; if (this.metadata && this.metadata.index) wantedIndex = this.metadata.index; let workerUnit = this.metadata && this.metadata.role && this.metadata.role == "worker"; let supportUnit = this.template.hasClass("Support"); this.trainers.sort(function(a, b) { // Prefer training buildings with short queues let aa = a.trainingQueueTime(); let bb = b.trainingQueueTime(); // Give priority to support units in the cc if (a.hasClass("Civic") && !supportUnit) aa += 10; if (b.hasClass("Civic") && !supportUnit) bb += 10; // And support units should not be too near to dangerous place if (supportUnit) { if (gameState.ai.HQ.isNearInvadingArmy(a.position())) aa += 50; if (gameState.ai.HQ.isNearInvadingArmy(b.position())) bb += 50; } // Give also priority to buildings with the right accessibility let aBase = a.getMetadata(PlayerID, "base"); let bBase = b.getMetadata(PlayerID, "base"); if (wantedIndex) { if (!aBase || gameState.ai.HQ.getBaseByID(aBase).accessIndex != wantedIndex) aa += 30; if (!bBase || gameState.ai.HQ.getBaseByID(bBase).accessIndex != wantedIndex) bb += 30; } // Then, if workers, small preference for bases with less workers if (workerUnit && aBase && bBase && aBase != bBase) { let apop = gameState.ai.HQ.getBaseByID(aBase).workers.length; let bpop = gameState.ai.HQ.getBaseByID(bBase).workers.length; if (apop > bpop) aa++; else if (bpop > apop) bb++; } return aa - bb; }); } if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0) this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base"); - this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata, this.promotedTypes(gameState)); + this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata); this.onStart(gameState); }; PETRA.TrainingPlan.prototype.addItem = function(amount = 1) { this.number += amount; }; -/** Find the promoted types corresponding to this.type */ -PETRA.TrainingPlan.prototype.promotedTypes = function(gameState) -{ - let types = []; - let promotion = this.template.promotion(); - let previous; - let template; - while (promotion) - { - types.push(promotion); - previous = promotion; - template = gameState.getTemplate(promotion); - if (!template) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" promotion template " + promotion + " is not found"); - promotion = undefined; - break; - } - promotion = template.promotion(); - if (previous === promotion) - { - if (gameState.ai.Config.debug > 0) - API3.warn(" unit " + promotion + " is its own promoted unit"); - promotion = undefined; - } - } - return types; -}; - PETRA.TrainingPlan.prototype.Serialize = function() { return { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "maxMerge": this.maxMerge }; }; PETRA.TrainingPlan.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; this.cost = new API3.Resources(); this.cost.Deserialize(data.cost); }; Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 26000) @@ -1,2157 +1,2163 @@ 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) { let cmpPlayer = QueryPlayerIDInterface(i); let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits); // Work out which phase we are in. let phase = ""; let cmpTechnologyManager = QueryPlayerIDInterface(i, 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": cmpPlayer.GetName(), "civ": cmpPlayer.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, "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : 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) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); if (!ent) return null; // All units must have a template; if not then it's a nonexistent entity id. let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; let ret = { "id": ent, "player": INVALID_PLAYER, "template": template }; 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(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "hasSomeFormation": cmpIdentity.HasSomeFormation(), "formations": cmpIdentity.GetFormationsList(), "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 = { - "entities": cmpProductionQueue.GetEntitiesList(), - "technologies": cmpProductionQueue.GetTechnologiesList(), - "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(), "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(), "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; let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { // Not a ranged attack, set some defaults. ret.attack[type].elevationBonus = 0; ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } ret.attack[type].elevationBonus = range.elevationBonus; 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, range.elevationBonus, 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 }; let elevationBonus = cmd.elevationBonus || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 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); 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); }; 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) { let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return {}; let ret = {}; for (let tech of cmpTechnologyManager.GetStartedTechs()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; - let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); - if (cmpProductionQueue) + const cmpResearcher = Engine.QueryInterface(ret[tech].researcher, IID_Researcher); + if (cmpResearcher) { - const research = cmpProductionQueue.GetQueue().find(item => item.technologyTemplate === tech); + const research = cmpResearcher.GetResearchingTechnologyByName(tech); ret[tech].progress = research.progress; ret[tech].timeRemaining = research.timeRemaining; ret[tech].paused = research.paused; } else { ret[tech].progress = 0; ret[tech].timeRemaining = 0; ret[tech].paused = true; } } return ret; }; /** * 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.Formation.FormationName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Formation.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) { - let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); - if (!cmpProductionQueue) - return 0; - - return cmpProductionQueue.GetBatchTime(data.batchSize); + 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/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 26000) @@ -1,959 +1,969 @@ function Player() {} Player.prototype.Schema = "" + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Don't serialize diplomacyColor or displayDiplomacyColor since they're modified by the GUI. */ Player.prototype.Serialize = function() { let state = {}; for (let key in this) if (this.hasOwnProperty(key)) state[key] = this[key]; state.diplomacyColor = undefined; state.displayDiplomacyColor = false; return state; }; Player.prototype.Deserialize = function(state) { for (let prop in state) this[prop] = state[prop]; }; /** * Which units will be shown with special icons at the top. */ var panelEntityClasses = "Hero Relic"; Player.prototype.Init = function() { this.playerID = undefined; this.name = undefined; // Define defaults elsewhere (supporting other languages). this.civ = undefined; this.color = undefined; this.diplomacyColor = undefined; this.displayDiplomacyColor = false; this.popUsed = 0; // Population of units owned or trained by this player. this.popBonuses = 0; // Sum of population bonuses of player's entities. this.maxPop = 300; // Maximum population. this.trainingBlocked = false; // Indicates whether any training queue is currently blocked. this.resourceCount = {}; this.resourceGatherers = {}; this.tradingGoods = []; // Goods for next trade-route and its probabilities * 100. this.team = -1; // Team number of the player, players on the same team will always have ally diplomatic status. Also this is useful for team emblems, scoring, etc. this.teamsLocked = false; this.state = "active"; // Game state. One of "active", "defeated", "won". this.diplomacy = []; // Array of diplomatic stances for this player with respect to other players (including gaia and self). this.sharedDropsites = false; this.formations = []; this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; this.cheatsEnabled = false; this.panelEntities = []; this.resourceNames = {}; this.disabledTemplates = {}; this.disabledTechnologies = {}; this.spyCostMultiplier = +this.template.SpyCostMultiplier; this.barterEntities = []; this.barterMultiplier = { "buy": clone(this.template.BarterMultiplier.Buy), "sell": clone(this.template.BarterMultiplier.Sell) }; // Initial resources. let resCodes = Resources.GetCodes(); for (let res of resCodes) { this.resourceCount[res] = 300; this.resourceNames[res] = Resources.GetResource(res).name; this.resourceGatherers[res] = 0; } // Trading goods probability in steps of 5. let resTradeCodes = Resources.GetTradableCodes(); let quotient = Math.floor(20 / resTradeCodes.length); let remainder = 20 % resTradeCodes.length; for (let i in resTradeCodes) this.tradingGoods.push({ "goods": resTradeCodes[i], "proba": 5 * (quotient + (+i < remainder ? 1 : 0)) }); }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPlayerID = function() { return this.playerID; }; Player.prototype.SetName = function(name) { this.name = name; }; Player.prototype.GetName = function() { return this.name; }; Player.prototype.SetCiv = function(civcode) { let oldCiv = this.civ; this.civ = civcode; // Normally, the civ is only set once. But in Atlas, map designers can change civs at any time. if (oldCiv && this.playerID && oldCiv != civcode) Engine.BroadcastMessage(MT_CivChanged, { "player": this.playerID, "from": oldCiv, "to": civcode }); }; Player.prototype.GetCiv = function() { return this.civ; }; Player.prototype.SetColor = function(r, g, b) { let colorInitialized = !!this.color; this.color = { "r": r / 255, "g": g / 255, "b": b / 255, "a": 1 }; // Used in Atlas. if (colorInitialized) Engine.BroadcastMessage(MT_PlayerColorChanged, { "player": this.playerID }); }; Player.prototype.SetDiplomacyColor = function(color) { this.diplomacyColor = { "r": color.r / 255, "g": color.g / 255, "b": color.b / 255, "a": 1 }; }; Player.prototype.SetDisplayDiplomacyColor = function(displayDiplomacyColor) { this.displayDiplomacyColor = displayDiplomacyColor; }; Player.prototype.GetColor = function() { return this.color; }; Player.prototype.GetDisplayedColor = function() { return this.displayDiplomacyColor ? this.diplomacyColor : this.color; }; // Try reserving num population slots. Returns 0 on success or number of missing slots otherwise. Player.prototype.TryReservePopulationSlots = function(num) { if (num != 0 && num > (this.GetPopulationLimit() - this.popUsed)) return num - (this.GetPopulationLimit() - this.popUsed); this.popUsed += num; return 0; }; Player.prototype.UnReservePopulationSlots = function(num) { this.popUsed -= num; }; Player.prototype.GetPopulationCount = function() { return this.popUsed; }; Player.prototype.AddPopulation = function(num) { this.popUsed += num; }; Player.prototype.SetPopulationBonuses = function(num) { this.popBonuses = num; }; Player.prototype.AddPopulationBonuses = function(num) { this.popBonuses += num; }; Player.prototype.GetPopulationLimit = function() { return Math.min(this.GetMaxPopulation(), this.popBonuses); }; Player.prototype.SetMaxPopulation = function(max) { this.maxPop = max; }; Player.prototype.GetMaxPopulation = function() { return Math.round(ApplyValueModificationsToEntity("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.CanBarter = function() { return this.barterEntities.length > 0; }; Player.prototype.GetBarterMultiplier = function() { return this.barterMultiplier; }; Player.prototype.GetSpyCostMultiplier = function() { return this.spyCostMultiplier; }; Player.prototype.GetPanelEntities = function() { return this.panelEntities; }; Player.prototype.IsTrainingBlocked = function() { return this.trainingBlocked; }; Player.prototype.BlockTraining = function() { this.trainingBlocked = true; }; Player.prototype.UnBlockTraining = function() { this.trainingBlocked = false; }; Player.prototype.SetResourceCounts = function(resources) { for (let res in resources) this.resourceCount[res] = resources[res]; }; Player.prototype.GetResourceCounts = function() { return this.resourceCount; }; Player.prototype.GetResourceGatherers = function() { return this.resourceGatherers; }; /** * @param {string} type - The generic type of resource to add the gatherer for. */ Player.prototype.AddResourceGatherer = function(type) { ++this.resourceGatherers[type]; }; /** * @param {string} type - The generic type of resource to remove the gatherer from. */ Player.prototype.RemoveResourceGatherer = function(type) { --this.resourceGatherers[type]; }; /** * Add resource of specified type to player. * @param {string} type - Generic type of resource. * @param {number} amount - Amount of resource, which should be added. */ Player.prototype.AddResource = function(type, amount) { this.resourceCount[type] += +amount; }; /** * Add resources to player. */ Player.prototype.AddResources = function(amounts) { for (let type in amounts) this.resourceCount[type] += +amounts[type]; }; Player.prototype.GetNeededResources = function(amounts) { // Check if we can afford it all. let amountsNeeded = {}; for (let type in amounts) if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type]) amountsNeeded[type] = amounts[type] - Math.floor(this.resourceCount[type]); if (Object.keys(amountsNeeded).length == 0) return undefined; return amountsNeeded; }; Player.prototype.SubtractResourcesOrNotify = function(amounts) { let amountsNeeded = this.GetNeededResources(amounts); // If we don't have enough resources, send a notification to the player. if (amountsNeeded) { let parameters = {}; let i = 0; for (let type in amountsNeeded) { ++i; parameters["resourceType" + i] = this.resourceNames[type]; parameters["resourceAmount" + i] = amountsNeeded[type]; } let msg = ""; // When marking strings for translations, you need to include the actual string, // not some way to derive the string. if (i < 1) warn("Amounts needed but no amounts given?"); else if (i == 1) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s"); else if (i == 2) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s"); else if (i == 3) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s"); else if (i == 4) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s, %(resourceAmount4)s %(resourceType4)s"); else warn("Localisation: Strings are not localised for more than 4 resources"); // Send as time-notification. let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [this.playerID], "message": msg, "parameters": parameters, "translateMessage": true, "translateParameters": { "resourceType1": "withinSentence", "resourceType2": "withinSentence", "resourceType3": "withinSentence", "resourceType4": "withinSentence" } }); return false; } for (let type in amounts) this.resourceCount[type] -= amounts[type]; return true; }; Player.prototype.TrySubtractResources = function(amounts) { if (!this.SubtractResourcesOrNotify(amounts)) return false; let cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (let type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]); return true; }; +Player.prototype.RefundResources = function(amounts) +{ + const cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); + if (cmpStatisticsTracker) + for (const type in amounts) + cmpStatisticsTracker.IncreaseResourceUsedCounter(type, -amounts[type]); + + this.AddResources(amounts); +}; + Player.prototype.GetNextTradingGoods = function() { let value = randFloat(0, 100); let last = this.tradingGoods.length - 1; let sumProba = 0; for (let i = 0; i < last; ++i) { sumProba += this.tradingGoods[i].proba; if (value < sumProba) return this.tradingGoods[i].goods; } return this.tradingGoods[last].goods; }; Player.prototype.GetTradingGoods = function() { let tradingGoods = {}; for (let resource of this.tradingGoods) tradingGoods[resource.goods] = resource.proba; return tradingGoods; }; Player.prototype.SetTradingGoods = function(tradingGoods) { let resTradeCodes = Resources.GetTradableCodes(); let sumProba = 0; for (let resource in tradingGoods) { if (resTradeCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0) { error("Invalid trading goods: " + uneval(tradingGoods)); return; } sumProba += tradingGoods[resource]; } if (sumProba != 100) { error("Invalid trading goods probability: " + uneval(sumProba)); return; } this.tradingGoods = []; for (let resource in tradingGoods) this.tradingGoods.push({ "goods": resource, "proba": tradingGoods[resource] }); }; Player.prototype.GetState = function() { return this.state; }; /** * @param {string} newState - Either "defeated" or "won". * @param {string|undefined} message - A string to be shown in chat, for example * markForTranslation("%(player)s has been defeated (failed objective)."). * If it is undefined, the caller MUST send that GUI notification manually. */ Player.prototype.SetState = function(newState, message) { if (this.state != "active") return; if (newState != "won" && newState != "defeated") { warn("Can't change playerstate to " + this.state); return; } if (!this.playerID) { warn("Gaia can't change state."); return; } this.state = newState; let won = newState == "won"; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (won) cmpRangeManager.SetLosRevealAll(this.playerID, true); else { // Reassign all player's entities to Gaia. let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); // The ownership change is done in two steps so that entities don't hit idle // (and thus possibly look for "enemies" to attack) before nearby allies get // converted to Gaia as well. for (let entity of entities) { let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for (let entity of entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); } Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID }); if (message) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": won ? "won" : "defeat", "players": [this.playerID], "allies": [this.playerID], "message": message }); } }; Player.prototype.GetTeam = function() { return this.team; }; Player.prototype.SetTeam = function(team) { if (this.teamsLocked) return; this.team = team; // Set all team members as allies. if (this.team != -1) { let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); if (this.team != cmpPlayer.GetTeam()) continue; this.SetAlly(i); cmpPlayer.SetAlly(this.playerID); } } Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetLockTeams = function(value) { this.teamsLocked = value; }; Player.prototype.GetLockTeams = function() { return this.teamsLocked; }; Player.prototype.GetDiplomacy = function() { return this.diplomacy.slice(); }; Player.prototype.SetDiplomacy = function(dipl) { this.diplomacy = dipl.slice(); Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetDiplomacyIndex = function(idx, value) { let cmpPlayer = QueryPlayerIDInterface(idx); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; this.diplomacy[idx] = value; Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": cmpPlayer.GetPlayerID() }); // Mutual worsening of relations. if (cmpPlayer.diplomacy[this.playerID] > value) cmpPlayer.SetDiplomacyIndex(this.playerID, value); }; Player.prototype.UpdateSharedLos = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); if (!cmpRangeManager || !cmpTechnologyManager) return; if (!cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech)) { cmpRangeManager.SetSharedLos(this.playerID, [this.playerID]); return; } cmpRangeManager.SetSharedLos(this.playerID, this.GetMutualAllies()); }; Player.prototype.GetFormations = function() { return this.formations; }; Player.prototype.SetFormations = function(formations) { this.formations = formations; }; Player.prototype.GetStartingCameraPos = function() { return this.startCam.position; }; Player.prototype.GetStartingCameraRot = function() { return this.startCam.rotation; }; Player.prototype.SetStartingCamera = function(pos, rot) { this.startCam = { "position": pos, "rotation": rot }; }; Player.prototype.HasStartingCamera = function() { return this.startCam !== undefined; }; Player.prototype.HasSharedLos = function() { let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech); }; Player.prototype.HasSharedDropsites = function() { return this.sharedDropsites; }; Player.prototype.SetControlAllUnits = function(c) { this.controlAllUnits = c; }; Player.prototype.CanControlAllUnits = function() { return this.controlAllUnits; }; Player.prototype.SetAI = function(flag) { this.isAI = flag; }; Player.prototype.IsAI = function() { return this.isAI; }; Player.prototype.GetPlayersByDiplomacy = function(func) { let players = []; for (let i = 0; i < this.diplomacy.length; ++i) if (this[func](i)) players.push(i); return players; }; Player.prototype.SetAlly = function(id) { this.SetDiplomacyIndex(id, 1); }; /** * Check if given player is our ally. */ Player.prototype.IsAlly = function(id) { return this.diplomacy[id] > 0; }; Player.prototype.GetAllies = function() { return this.GetPlayersByDiplomacy("IsAlly"); }; /** * Check if given player is our ally excluding ourself */ Player.prototype.IsExclusiveAlly = function(id) { return this.playerID != id && this.IsAlly(id); }; /** * Check if given player is our ally, and we are its ally */ Player.prototype.IsMutualAlly = function(id) { let cmpPlayer = QueryPlayerIDInterface(id); return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); }; Player.prototype.GetMutualAllies = function() { return this.GetPlayersByDiplomacy("IsMutualAlly"); }; /** * Check if given player is our ally, and we are its ally, excluding ourself */ Player.prototype.IsExclusiveMutualAlly = function(id) { return this.playerID != id && this.IsMutualAlly(id); }; Player.prototype.SetEnemy = function(id) { this.SetDiplomacyIndex(id, -1); }; /** * Check if given player is our enemy */ Player.prototype.IsEnemy = function(id) { return this.diplomacy[id] < 0; }; Player.prototype.GetEnemies = function() { return this.GetPlayersByDiplomacy("IsEnemy"); }; Player.prototype.SetNeutral = function(id) { this.SetDiplomacyIndex(id, 0); }; /** * Check if given player is neutral */ Player.prototype.IsNeutral = function(id) { return this.diplomacy[id] == 0; }; /** * Do some map dependant initializations */ Player.prototype.OnGlobalInitGame = function(msg) { // Replace the "{civ}" code with this civ ID. let disabledTemplates = this.disabledTemplates; this.disabledTemplates = {}; for (let template in disabledTemplates) if (disabledTemplates[template]) this.disabledTemplates[template.replace(/\{civ\}/g, this.civ)] = true; }; /** * Keep track of population effects of all entities that * become owned or unowned by this player. */ Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from != this.playerID && msg.to != this.playerID) return; let cmpCost = Engine.QueryInterface(msg.entity, IID_Cost); if (msg.from == this.playerID) { if (cmpCost) this.popUsed -= cmpCost.GetPopCost(); let panelIndex = this.panelEntities.indexOf(msg.entity); if (panelIndex >= 0) this.panelEntities.splice(panelIndex, 1); let barterIndex = this.barterEntities.indexOf(msg.entity); if (barterIndex >= 0) this.barterEntities.splice(barterIndex, 1); } if (msg.to == this.playerID) { if (cmpCost) this.popUsed += cmpCost.GetPopCost(); let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; if (MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses)) this.panelEntities.push(msg.entity); if (cmpIdentity.HasClass("Barter") && !Engine.QueryInterface(msg.entity, IID_Foundation)) this.barterEntities.push(msg.entity); } }; Player.prototype.OnResearchFinished = function(msg) { if (msg.tech == this.template.SharedLosTech) this.UpdateSharedLos(); else if (msg.tech == this.template.SharedDropsitesTech) this.sharedDropsites = true; }; Player.prototype.OnDiplomacyChanged = function() { this.UpdateSharedLos(); }; Player.prototype.OnValueModification = function(msg) { if (msg.component != "Player") return; if (msg.valueNames.indexOf("Player/SpyCostMultiplier") != -1) this.spyCostMultiplier = ApplyValueModificationsToEntity("Player/SpyCostMultiplier", +this.template.SpyCostMultiplier, this.entity); if (msg.valueNames.some(mod => mod.startsWith("Player/BarterMultiplier/"))) for (let res in this.template.BarterMultiplier.Buy) { this.barterMultiplier.buy[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Buy/"+res, +this.template.BarterMultiplier.Buy[res], this.entity); this.barterMultiplier.sell[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Sell/"+res, +this.template.BarterMultiplier.Sell[res], this.entity); } }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; Player.prototype.TributeResource = function(player, amounts) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; let resTribCodes = Resources.GetTributableCodes(); for (let resCode in amounts) if (resTribCodes.indexOf(resCode) == -1 || !Number.isInteger(amounts[resCode]) || amounts[resCode] < 0) { warn("Invalid tribute amounts: " + uneval(resCode) + ": " + uneval(amounts)); return; } if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); let total = Object.keys(amounts).reduce((sum, type) => sum + amounts[type], 0); let cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpOurStatisticsTracker) cmpOurStatisticsTracker.IncreaseTributesSentCounter(total); let cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpTheirStatisticsTracker) cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification({ "type": "tribute", "players": [player], "donator": this.playerID, "amounts": amounts }); Engine.BroadcastMessage(MT_TributeExchanged, { "to": player, "from": this.playerID, "amounts": amounts }); }; Player.prototype.AddDisabledTemplate = function(template) { this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTemplate = function(template) { this.disabledTemplates[template] = false; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTemplates = function(templates) { this.disabledTemplates = {}; for (let template of templates) this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTemplates = function() { return this.disabledTemplates; }; Player.prototype.AddDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = false; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTechnologies = function(techs) { this.disabledTechnologies = {}; for (let tech of techs) this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTechnologies = function() { return this.disabledTechnologies; }; Player.prototype.OnGlobalPlayerDefeated = function(msg) { let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : this.IsAlly(msg.playerId) ? "defeated_ally" : this.state === "won" ? "won" : "defeated_enemy"); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, this.playerID); }; Engine.RegisterComponentType(IID_Player, "Player", Player); Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 26000) @@ -1,1018 +1,518 @@ function ProductionQueue() {} ProductionQueue.prototype.Schema = - "Allows the building to train new units and research technologies" + - "" + - "0.7" + - "" + - "\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "tokens" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "tokens" + - "" + - "" + - "" + - "" + - "" + - Resources.BuildSchema("nonNegativeDecimal", ["time"]) + - ""; + "Helps the building to train new units and research technologies." + + ""; ProductionQueue.prototype.ProgressInterval = 1000; ProductionQueue.prototype.MaxQueueSize = 16; -ProductionQueue.prototype.Init = function() +/** + * This object represents an item in the queue. + */ +ProductionQueue.prototype.Item = function() {}; + +/** + * @param {number} producer - The entity ID of our producer. + * @param {string} metadata - Optionally any metadata attached to us. + */ +ProductionQueue.prototype.Item.prototype.Init = function(producer, metadata) { - this.nextID = 1; + this.producer = producer; + this.metadata = metadata; - this.queue = []; - /** - Queue items are: - { - "id": 1, - "player": 1, // Who paid for this batch; we need this to cope with refunds cleanly. - "productionStarted": false, // true iff production has started (we have reserved population). - "timeTotal": 15000, // msecs - "timeRemaining": 10000, // msecs - "paused": false, // Whether the item is currently paused (e.g. not the first item or parent is garrisoned). - "resources": { "wood": 100, ... }, // Total resources of the item when queued. - "entity": { - "template": "units/example", - "count": 10, - "neededSlots": 3, // Number of population slots missing for production to begin. - "population": 1, // Population per unit, multiply by count to get total. - "resources": { "wood": 100, ... }, // Resources per entity, multiply by count to get total. - "entityCache": [189, ...], // The entities created but not spawned yet. - }, - "technology": { - "template": "example_tech", - "resources": { "wood": 100, ... }, - } - } - */ }; -/* - * Returns list of entities that can be trained by this building. - */ -ProductionQueue.prototype.GetEntitiesList = function() +ProductionQueue.prototype.Item.prototype.QueueEntity = function(templateName, count) +{ + const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer); + if (!cmpTrainer) + return false; + this.entity = cmpTrainer.QueueBatch(templateName, count, this.metadata); + if (this.entity == -1) + return false; + this.originalItem = { + "templateName": templateName, + "count": count, + "metadata": this.metadata + }; + + return true; +}; + +ProductionQueue.prototype.Item.prototype.QueueTechnology = function(templateName) { - return Array.from(this.entitiesMap.values()); + const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher); + if (!cmpResearcher) + return false; + this.technology = cmpResearcher.QueueTechnology(templateName, this.metadata); + if (this.technology == -1) + return false; + + return true; }; /** - * @return {boolean} - Whether we are automatically queuing items. + * @param {number} id - The id of this item. */ -ProductionQueue.prototype.IsAutoQueueing = function() +ProductionQueue.prototype.Item.prototype.SetID = function(id) { - return !!this.autoqueuing; + this.id = id; +}; + +ProductionQueue.prototype.Item.prototype.Stop = function() +{ + if (this.entity) + { + const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer); + if (cmpTrainer) + cmpTrainer.StopBatch(this.entity); + } + + if (this.technology) + { + const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher); + if (cmpResearcher) + cmpResearcher.StopResearching(this.technology); + } }; /** - * Turn on Auto-Queue. + * Called when the first work is performed. */ -ProductionQueue.prototype.EnableAutoQueue = function() +ProductionQueue.prototype.Item.prototype.Start = function() { - this.autoqueuing = true; + this.started = true; +}; + +ProductionQueue.prototype.Item.prototype.IsStarted = function() +{ + return !!this.started; }; /** - * Turn off Auto-Queue. + * @return {boolean} - Whether this item is finished. */ -ProductionQueue.prototype.DisableAutoQueue = function() +ProductionQueue.prototype.Item.prototype.IsFinished = function() { - delete this.autoqueuing; + return !!this.finished; }; /** - * Calculate the new list of producible entities - * and update any entities currently being produced. + * @param {number} allocatedTime - The time allocated to this item. + * @return {number} - The time used for this item. */ -ProductionQueue.prototype.CalculateEntitiesMap = function() +ProductionQueue.prototype.Item.prototype.Progress = function(allocatedTime) { - // Don't reset the map, it's used below to update entities. - if (!this.entitiesMap) - this.entitiesMap = new Map(); - if (!this.template.Entities) - return; - - let string = this.template.Entities._string; - // Tokens can be added -> process an empty list to get them. - let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity); - if (!addedTokens && !string) - return; - - addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/); + if (this.entity) + { + const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer); + allocatedTime -= cmpTrainer.Progress(this.entity, allocatedTime); + if (!cmpTrainer.HasBatch(this.entity)) + delete this.entity; + } + if (this.technology) + { + const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher); + allocatedTime -= cmpResearcher.Progress(this.technology, allocatedTime); + if (!cmpResearcher.HasItem(this.technology)) + delete this.technology; + } + if (!this.entity && !this.technology) + this.finished = true; - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - let cmpPlayer = QueryOwnerInterface(this.entity); - let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - - let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {}; - - /** - * Process tokens: - * - process token modifiers (this is a bit tricky). - * - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID - * - remove disabled entities - * - upgrade templates where necessary - * This also updates currently queued production (it's more convenient to do it here). - */ - - let removeAllQueuedTemplate = (token) => { - let queue = clone(this.queue); - let template = this.entitiesMap.get(token); - for (let item of queue) - if (item.entity?.template && item.entity.template === template) - this.RemoveItem(item.id); - }; - let updateAllQueuedTemplate = (token, updateTo) => { - let template = this.entitiesMap.get(token); - for (let item of this.queue) - if (item.entity?.template && item.entity.template === template) - item.entity.template = updateTo; - }; + return allocatedTime; +}; - let toks = string.split(/\s+/); - for (let tok of addedTokens) - toks.push(tok); - - let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {}); - this.entitiesMap = toks.reduce((entMap, token) => { - let rawToken = token; - if (!(token in addedDict)) - { - // This is a bit wasteful but I can't think of a simpler/better way. - // The list of token is unlikely to be a performance bottleneck anyways. - token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity); - token = token.split(/\s+/); - if (token.every(tok => addedTokens.indexOf(tok) !== -1)) - { - removeAllQueuedTemplate(rawToken); - return entMap; - } - token = token[0]; - } - // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. - if (cmpIdentity) - token = token.replace(/\{native\}/g, cmpIdentity.GetCiv()); - if (cmpPlayer) - token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv()); +ProductionQueue.prototype.Item.prototype.Pause = function() +{ + this.paused = true; + if (this.entity) + Engine.QueryInterface(this.producer, IID_Trainer).PauseBatch(this.entity); + if (this.technology) + Engine.QueryInterface(this.producer, IID_Researcher).PauseTechnology(this.technology); +}; - // Filter out disabled and invalid entities. - if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token)) - { - removeAllQueuedTemplate(rawToken); - return entMap; - } +ProductionQueue.prototype.Item.prototype.Unpause = function() +{ + delete this.paused; + if (this.entity) + Engine.QueryInterface(this.producer, IID_Trainer).UnpauseBatch(this.entity); + if (this.technology) + Engine.QueryInterface(this.producer, IID_Researcher).UnpauseTechnology(this.technology); +}; - token = this.GetUpgradedTemplate(token); - entMap.set(rawToken, token); - updateAllQueuedTemplate(rawToken, token); - return entMap; - }, new Map()); +ProductionQueue.prototype.Item.prototype.IsPaused = function() +{ + return !!this.paused; }; -/* - * Returns the upgraded template name if necessary. +/** + * @return {Object} - Some basic information of this item. */ -ProductionQueue.prototype.GetUpgradedTemplate = function(templateName) +ProductionQueue.prototype.Item.prototype.GetBasicInfo = function() { - let cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) - return templateName; - - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - let template = cmpTemplateManager.GetTemplate(templateName); - while (template && template.Promotion !== undefined) - { - let requiredXp = ApplyValueModificationsToTemplate( - "Promotion/RequiredXp", - +template.Promotion.RequiredXp, - cmpPlayer.GetPlayerID(), - template); - if (requiredXp > 0) - break; - templateName = template.Promotion.Entity; - template = cmpTemplateManager.GetTemplate(templateName); - } - return templateName; + let result; + if (this.technology) + result = Engine.QueryInterface(this.producer, IID_Researcher).GetResearchingTechnology(this.technology); + else if (this.entity) + result = Engine.QueryInterface(this.producer, IID_Trainer).GetBatch(this.entity); + result.id = this.id; + result.paused = this.paused; + return result; }; -/* - * Returns list of technologies that can be researched by this building. +/** + * @return {Object} - The originally queued item. */ -ProductionQueue.prototype.GetTechnologiesList = function() +ProductionQueue.prototype.Item.prototype.OriginalItem = function() { - if (!this.template.Technologies) - return []; - - let string = this.template.Technologies._string; - string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity); - - if (!string) - return []; - - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (!cmpTechnologyManager) - return []; - - let cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) - return []; - - let techs = string.split(/\s+/); - - // Replace the civ specific technologies. - for (let i = 0; i < techs.length; ++i) - { - let tech = techs[i]; - if (tech.indexOf("{civ}") == -1) - continue; - let civTech = tech.replace("{civ}", cmpPlayer.GetCiv()); - techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); - } + return this.originalItem; +}; - // Remove any technologies that can't be researched by this civ. - techs = techs.filter(tech => - cmpTechnologyManager.CheckTechnologyRequirements( - DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()), - true)); +ProductionQueue.prototype.Item.prototype.Serialize = function() +{ + return { + "id": this.id, + "metadata": this.metadata, + "paused": this.paused, + "producer": this.producer, + "entity": this.entity, + "technology": this.technology, + "started": this.started, + "originalItem": this.originalItem + }; +}; - let techList = []; - // Stores the tech which supersedes the key. - let superseded = {}; +ProductionQueue.prototype.Item.prototype.Deserialize = function(data) +{ + this.Init(data.producer, data.metadata); - let disabledTechnologies = cmpPlayer.GetDisabledTechnologies(); + this.id = data.id; + this.paused = data.paused; + this.entity = data.entity; + this.technology = data.technology; + this.started = data.started; + this.originalItem = data.originalItem; +}; - // Add any top level technologies to an array which corresponds to the displayed icons. - // Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }. - for (let tech of techs) - { - if (disabledTechnologies && disabledTechnologies[tech]) - continue; +ProductionQueue.prototype.Init = function() +{ + this.nextID = 1; - let template = TechnologyTemplates.Get(tech); - if (!template.supersedes || techs.indexOf(template.supersedes) === -1) - techList.push(tech); - else - superseded[template.supersedes] = tech; - } + this.queue = []; +}; - // Now make researched/in progress techs invisible. - for (let i in techList) - { - let tech = techList[i]; - while (this.IsTechnologyResearchedOrInProgress(tech)) - tech = superseded[tech]; +ProductionQueue.prototype.Serialize = function() +{ + const queue = []; + for (const item of this.queue) + queue.push(item.Serialize()); + + return { + "autoqueuing": this.autoqueuing, + "nextID": this.nextID, + "paused": this.paused, + "timer": this.timer, + "queue": queue + }; +}; - techList[i] = tech; - } +ProductionQueue.prototype.Deserialize = function(data) +{ + this.Init(); - let ret = []; + this.autoqueuing = data.autoqueuing; + this.nextID = data.nextID; + this.paused = data.paused; + this.timer = data.timer; - // This inserts the techs into the correct positions to line up the technology pairs. - for (let i = 0; i < techList.length; ++i) + for (const item of data.queue) { - let tech = techList[i]; - if (!tech) - { - ret[i] = undefined; - continue; - } - - let template = TechnologyTemplates.Get(tech); - if (template.top) - ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom }; - else - ret[i] = tech; + const newItem = new this.Item(); + newItem.Deserialize(item); + this.queue.push(newItem); } - - return ret; }; -ProductionQueue.prototype.GetTechCostMultiplier = function() +/** + * @return {boolean} - Whether we are automatically queuing items. + */ +ProductionQueue.prototype.IsAutoQueueing = function() { - let techCostMultiplier = {}; - for (let res in this.template.TechCostMultiplier) - techCostMultiplier[res] = ApplyValueModificationsToEntity( - "ProductionQueue/TechCostMultiplier/" + res, - +this.template.TechCostMultiplier[res], - this.entity); - - return techCostMultiplier; + return !!this.autoqueuing; }; -ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech) +/** + * Turn on Auto-Queue. + */ +ProductionQueue.prototype.EnableAutoQueue = function() { - if (!tech) - return false; - - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (!cmpTechnologyManager) - return false; - - let template = TechnologyTemplates.Get(tech); - if (template.top) - return cmpTechnologyManager.IsTechnologyResearched(template.top) || - cmpTechnologyManager.IsInProgress(template.top) || - cmpTechnologyManager.IsTechnologyResearched(template.bottom) || - cmpTechnologyManager.IsInProgress(template.bottom); + this.autoqueuing = true; +}; - return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech); +/** + * Turn off Auto-Queue. + */ +ProductionQueue.prototype.DisableAutoQueue = function() +{ + delete this.autoqueuing; }; /* * Adds a new batch of identical units to train or a technology to research to the production queue. * @param {string} templateName - The template to start production on. * @param {string} type - The type of production (i.e. "unit" or "technology"). * @param {number} count - The amount of units to be produced. Ignored for a tech. * @param {any} metadata - Optionally any metadata to be attached to the item. * @param {boolean} pushFront - Whether to push the item to the front of the queue and pause any item(s) currently in progress. * * @return {boolean} - Whether the addition of the item has succeeded. */ ProductionQueue.prototype.AddItem = function(templateName, type, count, metadata, pushFront = false) { // TODO: there should be a way for the GUI to determine whether it's going // to be possible to add a batch (based on resource costs and length limits). - let cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) - return false; - let player = cmpPlayer.GetPlayerID(); if (!this.queue.length) { - let cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade); + const cmpPlayer = QueryOwnerInterface(this.entity); + if (!cmpPlayer) + return false; + const player = cmpPlayer.GetPlayerID(); + const cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade); if (cmpUpgrade && cmpUpgrade.IsUpgrading()) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("Entity is being upgraded. Cannot start production."), "translateMessage": true }); return false; } } else if (this.queue.length >= this.MaxQueueSize) { - let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + const cmpPlayer = QueryOwnerInterface(this.entity); + if (!cmpPlayer) + return false; + const player = cmpPlayer.GetPlayerID(); + const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("The production queue is full."), "translateMessage": true, }); return false; } - const item = { - "player": player, - "metadata": metadata, - "productionStarted": false, - "resources": {}, // The total resource costs. - "paused": false - }; - - // ToDo: Still some duplication here, some can might be combined, - // but requires some more refactoring. + const item = new this.Item(); + item.Init(this.entity, metadata); if (type == "unit") { - if (!Number.isInteger(count) || count <= 0) - { - error("Invalid batch count " + count); - return false; - } - - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - let template = cmpTemplateManager.GetTemplate(this.GetUpgradedTemplate(templateName)); - if (!template) + if (!item.QueueEntity(templateName, count)) return false; - - item.entity = { - "template": templateName, - "count": count, - "population": ApplyValueModificationsToTemplate( - "Cost/Population", - +template.Cost.Population, - player, - template), - "resources": {}, // The resource costs per entity. - }; - - for (let res in template.Cost.Resources) - { - item.entity.resources[res] = ApplyValueModificationsToTemplate( - "Cost/Resources/" + res, - +template.Cost.Resources[res], - player, - template); - - item.resources[res] = Math.floor(count * item.entity.resources[res]); - } - - if (template.TrainingRestrictions) - { - let unitCategory = template.TrainingRestrictions.Category; - let cmpPlayerEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); - if (cmpPlayerEntityLimits) - { - if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, count, templateName, template.TrainingRestrictions.MatchLimit)) - // Already warned, return. - return false; - cmpPlayerEntityLimits.ChangeCount(unitCategory, count); - if (template.TrainingRestrictions.MatchLimit) - cmpPlayerEntityLimits.ChangeMatchCount(templateName, count); - } - } - - const buildTime = ApplyValueModificationsToTemplate( - "Cost/BuildTime", - +template.Cost.BuildTime, - player, - template); - const time = this.GetBatchTime(count) * buildTime * 1000; - item.timeTotal = time; - item.timeRemaining = time; } else if (type == "technology") { - if (!TechnologyTemplates.Has(templateName)) - return false; - - if (!this.GetTechnologiesList().some(tech => - tech && - (tech == templateName || - tech.pair && - (tech.top == templateName || tech.bottom == templateName)))) - { - error("This entity cannot research " + templateName); + if (!item.QueueTechnology(templateName)) return false; - } - - item.technology = { - "template": templateName, - "resources": {} - }; - - let template = TechnologyTemplates.Get(templateName); - let techCostMultiplier = this.GetTechCostMultiplier(); - - if (template.cost) - for (const res in template.cost) - { - item.technology.resources[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]); - item.resources[res] = item.technology.resources[res]; - } - - const time = techCostMultiplier.time * (template.researchTime || 0) * 1000; - item.timeTotal = time; - item.timeRemaining = time; } else { warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue"); return false; } - // TrySubtractResources should report error to player (they ran out of resources). - if (!cmpPlayer.TrySubtractResources(item.resources)) - return false; - - item.id = this.nextID++; + item.SetID(this.nextID++); if (pushFront) { - if (this.queue[0]) - this.queue[0].paused = true; + this.queue[0]?.Pause(); this.queue.unshift(item); } else this.queue.push(item); - const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); - if (item.entity) - cmpTrigger.CallEvent("OnTrainingQueued", { - "playerid": player, - "unitTemplate": item.entity.template, - "count": count, - "metadata": metadata, - "trainerEntity": this.entity - }); - if (item.technology) - { - // Tell the technology manager that we have started researching this - // such that players can't research the same thing twice. - const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - cmpTechnologyManager.QueuedResearch(templateName, this.entity); - - cmpTrigger.CallEvent("OnResearchQueued", { - "playerid": player, - "technologyTemplate": item.technology.template, - "researcherEntity": this.entity - }); - } - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); if (!this.timer) this.StartTimer(); return true; }; /* * Removes an item from the queue. - * Refunds resource costs and population reservations. - * item.player is used as this.entity's owner may have changed. */ ProductionQueue.prototype.RemoveItem = function(id) { let itemIndex = this.queue.findIndex(item => item.id == id); if (itemIndex == -1) return; - let item = this.queue[itemIndex]; - - // Destroy any cached entities (those which didn't spawn for some reason). - if (item.entity?.cache?.length) - { - for (const ent of item.entity.cache) - Engine.DestroyEntity(ent); - - delete item.entity.cache; - } - - const cmpPlayer = QueryPlayerIDInterface(item.player); - - if (item.entity) - { - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - const template = cmpTemplateManager.GetTemplate(item.entity.template); - if (template.TrainingRestrictions) - { - let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); - if (cmpPlayerEntityLimits) - cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.entity.count); - if (template.TrainingRestrictions.MatchLimit) - cmpPlayerEntityLimits.ChangeMatchCount(item.entity.template, -item.entity.count); - } - if (cmpPlayer) - { - if (item.productionStarted) - cmpPlayer.UnReservePopulationSlots(item.entity.population * item.entity.count); - if (itemIndex == 0) - cmpPlayer.UnBlockTraining(); - } - } - - let cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker); - - const totalCosts = {}; - for (let resource in item.resources) - { - totalCosts[resource] = 0; - if (item.entity) - totalCosts[resource] += Math.floor(item.entity.count * item.entity.resources[resource]); - if (item.technology) - totalCosts[resource] += item.technology.resources[resource]; - if (cmpStatisticsTracker) - cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]); - } - - if (cmpPlayer) - cmpPlayer.AddResources(totalCosts); - - if (item.technology) - { - let cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager); - if (cmpTechnologyManager) - cmpTechnologyManager.StoppedResearch(item.technology.template, true); - } + this.queue.splice(itemIndex, 1)[0].Stop(); - this.queue.splice(itemIndex, 1); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); if (!this.queue.length) this.StopTimer(); }; ProductionQueue.prototype.SetAnimation = function(name) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation(name, false, 1); }; /* * Returns basic data from all batches in the production queue. */ ProductionQueue.prototype.GetQueue = function() { - return this.queue.map(item => ({ - "id": item.id, - "unitTemplate": item.entity?.template, - "technologyTemplate": item.technology?.template, - "count": item.entity?.count, - "neededSlots": item.entity?.neededSlots, - "progress": 1 - (item.timeRemaining / (item.timeTotal || 1)), - "timeRemaining": item.timeRemaining, - "paused": item.paused, - "metadata": item.metadata - })); + return this.queue.map(item => item.GetBasicInfo()); }; /* * Removes all existing batches from the queue. */ ProductionQueue.prototype.ResetQueue = function() { while (this.queue.length) this.RemoveItem(this.queue[0].id); this.DisableAutoQueue(); }; /* - * Returns batch build time. - */ -ProductionQueue.prototype.GetBatchTime = function(batchSize) -{ - // TODO: work out what equation we should use here. - return Math.pow(batchSize, ApplyValueModificationsToEntity( - "ProductionQueue/BatchTimeModifier", - +this.template.BatchTimeModifier, - this.entity)); -}; - -ProductionQueue.prototype.OnOwnershipChanged = function(msg) -{ - // Reset the production queue whenever the owner changes. - // (This should prevent players getting surprised when they capture - // an enemy building, and then loads of the enemy's civ's soldiers get - // created from it. Also it means we don't have to worry about - // updating the reserved pop slots.) - this.ResetQueue(); - - if (msg.to != INVALID_PLAYER) - this.CalculateEntitiesMap(); -}; - -ProductionQueue.prototype.OnCivChanged = function() -{ - this.CalculateEntitiesMap(); -}; - -/* - * This function creates the entities and places them in world if possible - * (some of these entities may be garrisoned directly if autogarrison, the others are spawned). - * @param {Object} item - The item to spawn units for. - * @param {number} item.entity.count - The number of entities to spawn. - * @param {string} item.player - The owner of the item. - * @param {string} item.entity.template - The template to spawn. - * @param {any} - item.metadata - Optionally any metadata to add to the TrainingFinished message. - * - * @return {number} - The number of successfully created entities - */ -ProductionQueue.prototype.SpawnUnits = function(item) -{ - let createdEnts = []; - let spawnedEnts = []; - - // We need entities to test spawning, but we don't want to waste resources, - // so only create them once and use as needed. - if (!item.entity.cache) - { - item.entity.cache = []; - for (let i = 0; i < item.entity.count; ++i) - item.entity.cache.push(Engine.AddEntity(item.entity.template)); - } - - let autoGarrison; - let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); - if (cmpRallyPoint) - { - let data = cmpRallyPoint.GetData()[0]; - if (data && data.target && data.target == this.entity && data.command == "garrison") - autoGarrison = true; - } - - let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); - let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - let positionSelf = cmpPosition && cmpPosition.GetPosition(); - - let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); - let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker); - while (item.entity.cache.length) - { - const ent = item.entity.cache[0]; - let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); - let garrisoned = false; - - if (autoGarrison) - { - let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); - if (cmpGarrisonable) - { - // Temporary owner affectation needed for GarrisonHolder checks. - cmpNewOwnership.SetOwnerQuiet(item.player); - garrisoned = cmpGarrisonable.Garrison(this.entity); - cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER); - } - } - - if (!garrisoned) - { - let pos = cmpFootprint.PickSpawnPoint(ent); - if (pos.y < 0) - break; - - let cmpNewPosition = Engine.QueryInterface(ent, IID_Position); - cmpNewPosition.JumpTo(pos.x, pos.z); - - if (positionSelf) - cmpNewPosition.SetYRotation(positionSelf.horizAngleTo(pos)); - - spawnedEnts.push(ent); - } - - // Decrement entity count in the EntityLimits component - // since it will be increased by EntityLimits.OnGlobalOwnershipChanged, - // i.e. we replace a 'trained' entity by 'alive' one. - // Must be done after spawn check so EntityLimits decrements only if unit spawns. - if (cmpPlayerEntityLimits) - { - let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); - if (cmpTrainingRestrictions) - cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1); - } - cmpNewOwnership.SetOwner(item.player); - - if (cmpPlayerStatisticsTracker) - cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent); - - item.entity.cache.shift(); - createdEnts.push(ent); - } - - if (spawnedEnts.length && !autoGarrison && cmpRallyPoint) - for (let com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts)) - ProcessCommand(item.player, com); - - if (createdEnts.length) - { - // Play a sound, but only for the first in the batch (to avoid nasty phasing effects). - PlaySound("trained", createdEnts[0]); - Engine.PostMessage(this.entity, MT_TrainingFinished, { - "entities": createdEnts, - "owner": item.player, - "metadata": item.metadata - }); - } - - return createdEnts.length; -}; - -/* - * Increments progress on the first item in the production queue and blocks the - * queue if population limit is reached or some units failed to spawn. + * Increments progress on the first item in the production queue. * @param {Object} data - Unused in this case. * @param {number} lateness - The time passed since the expected time to fire the function. */ ProductionQueue.prototype.ProgressTimeout = function(data, lateness) { if (this.paused) return; - let cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) - return; - // Allocate available time to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second). let time = this.ProgressInterval + lateness; - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); while (this.queue.length) { let item = this.queue[0]; - if (item.paused) - item.paused = false; - if (!item.productionStarted) + if (item.IsPaused()) + item.Unpause(); + if (!item.IsStarted()) { if (item.entity) - { - const template = cmpTemplateManager.GetTemplate(item.entity.template); - item.entity.population = ApplyValueModificationsToTemplate( - "Cost/Population", - +template.Cost.Population, - item.player, - template); - - item.entity.neededSlots = cmpPlayer.TryReservePopulationSlots(item.entity.population * item.entity.count); - if (item.entity.neededSlots) - { - cmpPlayer.BlockTraining(); - return; - } this.SetAnimation("training"); - - cmpPlayer.UnBlockTraining(); - - Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity }); - } if (item.technology) - { - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (cmpTechnologyManager) - cmpTechnologyManager.StartedResearch(item.technology.template, true); - else - warn("Failed to start researching " + item.technology.template + ": No TechnologyManager available."); - this.SetAnimation("researching"); - } - item.productionStarted = true; + item.Start(); } - - if (item.timeRemaining > time) + time -= item.Progress(time); + if (!item.IsFinished()) { - item.timeRemaining -= time; Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); return; } - if (item.entity) - { - let numSpawned = this.SpawnUnits(item); - if (numSpawned) - cmpPlayer.UnReservePopulationSlots(item.entity.population * numSpawned); - if (numSpawned == item.entity.count) - { - cmpPlayer.UnBlockTraining(); - delete this.spawnNotified; - } - else - { - if (numSpawned) - { - item.entity.count -= numSpawned; - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); - } - - cmpPlayer.BlockTraining(); - - if (!this.spawnNotified) - { - let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGUIInterface.PushNotification({ - "players": [cmpPlayer.GetPlayerID()], - "message": markForTranslation("Can't find free space to spawn trained units"), - "translateMessage": true - }); - this.spawnNotified = true; - } - return; - } - } - if (item.technology) - { - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (cmpTechnologyManager) - cmpTechnologyManager.ResearchTechnology(item.technology.template); - else - warn("Failed to finish researching " + item.technology.template + ": No TechnologyManager available."); - - const template = TechnologyTemplates.Get(item.technology.template); - if (template && template.soundComplete) - { - let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); - if (cmpSoundManager) - cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity); - } - } - - time -= item.timeRemaining; this.queue.shift(); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); // If autoqueuing, push a new unit on the queue immediately, // but don't start right away. This 'wastes' some time, making // autoqueue slightly worse than regular queuing, and also ensures // that autoqueue doesn't train more than one item per turn, // if the units would take fewer than ProgressInterval ms to train. - if (this.autoqueuing && item.entity) + if (this.autoqueuing) { - if (!this.AddItem(item.entity.template, "unit", item.entity.count, item.metadata)) + const autoqueueData = item.OriginalItem(); + if (!autoqueueData) + continue; + + if (!this.AddItem(autoqueueData.templateName, "unit", autoqueueData.count, autoqueueData.metadata)) { this.DisableAutoQueue(); const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ - "players": [cmpPlayer.GetPlayerID()], + "players": [QueryOwnerInterface(this.entity).GetPlayerID()], "message": markForTranslation("Could not auto-queue unit, de-activating."), "translateMessage": true }); } break; } } if (!this.queue.length) this.StopTimer(); }; ProductionQueue.prototype.PauseProduction = function() { this.StopTimer(); this.paused = true; - if (this.queue[0]) - this.queue[0].paused = true; + this.queue[0]?.Pause(); + this.StopTimer(); }; ProductionQueue.prototype.UnpauseProduction = function() { - if (this.queue[0]) - this.queue[0].paused = false; + this.queue[0]?.Unpause(); delete this.paused; this.StartTimer(); }; ProductionQueue.prototype.StartTimer = function() { if (this.timer) return; this.timer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetInterval( this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, this.ProgressInterval, null ); }; ProductionQueue.prototype.StopTimer = function() { if (!this.timer) return; this.SetAnimation("idle"); Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.timer); delete this.timer; }; -ProductionQueue.prototype.OnValueModification = function(msg) -{ - // If the promotion requirements of units is changed, - // update the entities list so that automatically promoted units are shown - // appropriately in the list. - if (msg.component != "Promotion" && (msg.component != "ProductionQueue" || - !msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/")))) - return; - - if (msg.entities.indexOf(this.entity) === -1) - return; - - // This also updates the queued production if necessary. - this.CalculateEntitiesMap(); - - // Inform the GUI that it'll need to recompute the selection panel. - // TODO: it would be better to only send the message if something actually changing - // for the current production queue. - let cmpPlayer = QueryOwnerInterface(this.entity); - if (cmpPlayer) - Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); -}; - ProductionQueue.prototype.HasQueuedProduction = function() { return this.queue.length > 0; }; -ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg) +ProductionQueue.prototype.OnOwnershipChanged = function(msg) { - this.CalculateEntitiesMap(); + // Reset the production queue whenever the owner changes. + // (This should prevent players getting surprised when they capture + // an enemy building, and then loads of the enemy's civ's soldiers get + // created from it. Also it means we don't have to worry about + // updating the reserved pop slots.) + this.ResetQueue(); }; ProductionQueue.prototype.OnGarrisonedStateChanged = function(msg) { if (msg.holderID != INVALID_ENTITY) this.PauseProduction(); else this.UnpauseProduction(); }; Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue); Index: ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js (revision 26000) @@ -0,0 +1,447 @@ +function Researcher() {} + +Researcher.prototype.Schema = + "Allows the entity to research technologies." + + "" + + "" + + "0.5" + + "0.1" + + "0" + + "2" + + "" + + "" + + "" + + "\n phase_town_{civ}\n phase_metropolis_ptol\n unlock_shared_los\n wonder_population_cap\n " + + "" + + "" + + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + + "" + + "" + + "" + + Resources.BuildSchema("nonNegativeDecimal", ["time"]) + + "" + + ""; + +/** + * This object represents a technology being researched. + */ +Researcher.prototype.Item = function() {}; + +/** + * @param {string} templateName - The name of the template we ought to research. + * @param {number} researcher - The entity ID of our researcher. + * @param {string} metadata - Optionally any metadata to attach to us. + */ +Researcher.prototype.Item.prototype.Init = function(templateName, researcher, metadata) +{ + this.templateName = templateName; + this.researcher = researcher; + this.metadata = metadata; +}; + +/** + * Prepare for the queue. + * @param {Object} techCostMultiplier - The multipliers to use when calculating costs. + * @return {boolean} - Whether the item was successfully initiated. + */ +Researcher.prototype.Item.prototype.Queue = function(techCostMultiplier) +{ + const template = TechnologyTemplates.Get(this.templateName); + if (!template) + return false; + + this.resources = {}; + + if (template.cost) + for (const res in template.cost) + this.resources[res] = Math.floor((techCostMultiplier[res] === undefined ? 1 : techCostMultiplier[res]) * template.cost[res]); + + const cmpPlayer = QueryOwnerInterface(this.researcher); + + // TrySubtractResources should report error to player (they ran out of resources). + if (!cmpPlayer?.TrySubtractResources(this.resources)) + return false; + this.player = cmpPlayer.GetPlayerID(); + + const time = (techCostMultiplier.time || 1) * (template.researchTime || 0) * 1000; + this.timeRemaining = time; + this.timeTotal = time; + + // Tell the technology manager that we have queued researching this + // such that players can't research the same thing twice. + const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); + cmpTechnologyManager.QueuedResearch(this.templateName, this.researcher); + + return true; +}; + +Researcher.prototype.Item.prototype.Stop = function() +{ + const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); + if (cmpTechnologyManager) + cmpTechnologyManager.StoppedResearch(this.templateName, true); + + QueryPlayerIDInterface(this.player)?.RefundResources(this.resources); + delete this.resources; +}; + +/** + * Called when the first work is performed. + */ +Researcher.prototype.Item.prototype.Start = function() +{ + const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); + cmpTechnologyManager.StartedResearch(this.templateName, true); + this.started = true; +}; + +Researcher.prototype.Item.prototype.Finish = function() +{ + const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); + cmpTechnologyManager.ResearchTechnology(this.templateName); + + const template = TechnologyTemplates.Get(this.templateName); + if (template?.soundComplete) + Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher); + this.finished = true; +}; + +/** + * @param {number} allocatedTime - The time allocated to this item. + * @return {number} - The time used for this item. + */ +Researcher.prototype.Item.prototype.Progress = function(allocatedTime) +{ + if (!this.started) + this.Start(); + + if (this.timeRemaining > allocatedTime) + { + this.timeRemaining -= allocatedTime; + return allocatedTime; + } + this.Finish(); + return this.timeRemaining; +}; + +Researcher.prototype.Item.prototype.Pause = function() +{ + this.paused = true; +}; + +Researcher.prototype.Item.prototype.Unpause = function() +{ + delete this.paused; +}; + +/** + * @return {Object} - Some basic information of this item. + */ +Researcher.prototype.Item.prototype.GetBasicInfo = function() +{ + return { + "technologyTemplate": this.templateName, + "progress": 1 - (this.timeRemaining / this.timeTotal), + "timeRemaining": this.timeRemaining, + "paused": this.paused, + "metadata": this.metadata + }; +}; + +Researcher.prototype.Item.prototype.Serialize = function(id) +{ + return { + "id": id, + "metadata": this.metadata, + "paused": this.paused, + "player": this.player, + "researcher": this.researcher, + "resource": this.resources, + "started": this.started, + "templateName": this.templateName, + "timeRemaining": this.timeRemaining, + "timeTotal": this.timeTotal, + }; +}; + +Researcher.prototype.Item.prototype.Deserialize = function(data) +{ + this.Init(data.templateName, data.researcher, data.metadata); + + this.paused = data.paused; + this.player = data.player; + this.researcher = data.researcher; + this.resources = data.resources; + this.started = data.started; + this.timeRemaining = data.timeRemaining; + this.timeTotal = data.timeTotal; +}; + +Researcher.prototype.Init = function() +{ + this.nextID = 1; + this.queue = new Map(); +}; + +Researcher.prototype.Serialize = function() +{ + const queue = []; + for (const [id, item] of this.queue) + queue.push(item.Serialize(id)); + + return { + "nextID": this.nextID, + "queue": queue + }; +}; + +Researcher.prototype.Deserialize = function(data) +{ + this.Init(); + this.nextID = data.nextID; + for (const item of data.queue) + { + const newItem = new this.Item(); + newItem.Deserialize(item); + this.queue.set(item.id, newItem); + } +}; + +/* + * Returns list of technologies that can be researched by this entity. + */ +Researcher.prototype.GetTechnologiesList = function() +{ + if (!this.template.Technologies) + return []; + + let string = this.template.Technologies._string; + string = ApplyValueModificationsToEntity("Researcher/Technologies/_string", string, this.entity); + + if (!string) + return []; + + const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); + if (!cmpTechnologyManager) + return []; + + const cmpPlayer = QueryOwnerInterface(this.entity); + if (!cmpPlayer) + return []; + + let techs = string.split(/\s+/); + + // Replace the civ specific technologies. + for (let i = 0; i < techs.length; ++i) + { + const tech = techs[i]; + if (tech.indexOf("{civ}") == -1) + continue; + const civTech = tech.replace("{civ}", cmpPlayer.GetCiv()); + techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); + } + + // Remove any technologies that can't be researched by this civ. + techs = techs.filter(tech => + cmpTechnologyManager.CheckTechnologyRequirements( + DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()), + true)); + + const techList = []; + const superseded = {}; + + const disabledTechnologies = cmpPlayer.GetDisabledTechnologies(); + + // Add any top level technologies to an array which corresponds to the displayed icons. + // Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }. + for (const tech of techs) + { + if (disabledTechnologies && disabledTechnologies[tech]) + continue; + + const template = TechnologyTemplates.Get(tech); + if (!template.supersedes || techs.indexOf(template.supersedes) === -1) + techList.push(tech); + else + superseded[template.supersedes] = tech; + } + + // Now make researched/in progress techs invisible. + for (const i in techList) + { + let tech = techList[i]; + while (this.IsTechnologyResearchedOrInProgress(tech)) + tech = superseded[tech]; + + techList[i] = tech; + } + + const ret = []; + + // This inserts the techs into the correct positions to line up the technology pairs. + for (let i = 0; i < techList.length; ++i) + { + const tech = techList[i]; + if (!tech) + { + ret[i] = undefined; + continue; + } + + const template = TechnologyTemplates.Get(tech); + if (template.top) + ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom }; + else + ret[i] = tech; + } + + return ret; +}; + +/** + * @return {Object} - The multipliers to change the costs of any research with. + */ +Researcher.prototype.GetTechCostMultiplier = function() +{ + const techCostMultiplier = {}; + for (const res in this.template.TechCostMultiplier) + techCostMultiplier[res] = ApplyValueModificationsToEntity( + "Researcher/TechCostMultiplier/" + res, + +this.template.TechCostMultiplier[res], + this.entity); + + return techCostMultiplier; +}; + +/** + * Checks whether we can research the given technology, minding paired techs. + */ +Researcher.prototype.IsTechnologyResearchedOrInProgress = function(tech) +{ + if (!tech) + return false; + + const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); + if (!cmpTechnologyManager) + return false; + + const template = TechnologyTemplates.Get(tech); + if (template.top) + return cmpTechnologyManager.IsTechnologyResearched(template.top) || + cmpTechnologyManager.IsInProgress(template.top) || + cmpTechnologyManager.IsTechnologyResearched(template.bottom) || + cmpTechnologyManager.IsInProgress(template.bottom); + + return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech); +}; + +/** + * @param {string} templateName - The technology to queue. + * @param {string} metadata - Any metadata attached to the item. + * @return {number} - The ID of the item. -1 if the item could not be researched. + */ +Researcher.prototype.QueueTechnology = function(templateName, metadata) +{ + if (!this.GetTechnologiesList().some(tech => + tech && (tech == templateName || + tech.pair && (tech.top == templateName || tech.bottom == templateName)))) + { + error("This entity cannot research " + templateName + "."); + return -1; + } + + const item = new this.Item(); + item.Init(templateName, this.entity, metadata); + + const techCostMultiplier = this.GetTechCostMultiplier(); + if (!item.Queue(techCostMultiplier)) + return -1; + + const id = this.nextID++; + this.queue.set(id, item); + return id; +}; + +/** + * @param {number} id - The id of the technology researched here we need to stop. + */ +Researcher.prototype.StopResearching = function(id) +{ + this.queue.get(id).Stop(); + this.queue.delete(id); +}; + +/** + * @param {number} id - The id of the technology. + */ +Researcher.prototype.PauseTechnology = function(id) +{ + this.queue.get(id).Pause(); +}; + +/** + * @param {number} id - The id of the technology. + */ +Researcher.prototype.UnpauseTechnology = function(id) +{ + this.queue.get(id).Unpause(); +}; + +/** + * @param {number} id - The ID of the item to check. + * @return {boolean} - Whether we are currently training the item. + */ +Researcher.prototype.HasItem = function(id) +{ + return this.queue.has(id); +}; + +/** + * @parameter {number} id - The id of the research. + * @return {Object} - Some basic information about the research. + */ +Researcher.prototype.GetResearchingTechnology = function(id) +{ + return this.queue.get(id).GetBasicInfo(); +}; + +/** + * @parameter {string} technologyName - The name of the research. + * @return {Object} - Some basic information about the research. + */ +Researcher.prototype.GetResearchingTechnologyByName = function(technologyName) +{ + let techID; + for (const [id, value] of this.queue) + if (value.templateName === technologyName) + { + techID = id; + break; + } + if (!techID) + return undefined; + + return this.GetResearchingTechnology(techID); +}; + +/** + * @param {number} id - The ID of the item we spent time on. + * @param {number} allocatedTime - The time we spent on the given item. + * @return {number} - The time we've actually used. + */ +Researcher.prototype.Progress = function(id, allocatedTime) +{ + const item = this.queue.get(id); + const usedTime = item.Progress(allocatedTime); + if (item.finished) + this.queue.delete(id); + return usedTime; +}; + +Engine.RegisterComponentType(IID_Researcher, "Researcher", Researcher); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/Researcher.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/plain \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 26000) @@ -1,376 +1,387 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the entityID of the researcher. this.researchQueued = new Map(); // Holds technologies which are being researched currently (non-queued). this.researchStarted = new Set(); this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.unresearchedAutoResearchTechs = new Set(); let allTechs = TechnologyTemplates.GetAll(); for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(key); }; TechnologyManager.prototype.OnUpdate = function() { this.UpdateAutoResearch(); }; // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { for (let key of this.unresearchedAutoResearchTechs) { let tech = TechnologyTemplates.Get(key); if ((tech.autoResearch && this.CanResearch(key)) || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } }; // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function(templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; TechnologyManager.prototype.IsTechnologyStarted = function(tech) { return this.researchStarted.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; if (this.IsTechnologyResearched(tech)) return false; return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {Object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg) { // This automatically updates classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (let cls of classes) { this.classCounts[cls] = this.classCounts[cls] || 0; this.classCounts[cls] += 1; this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; this.typeCountsByClass[cls][template] += 1; } } } if (msg.from == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (let cls of classes) { this.classCounts[cls] -= 1; if (this.classCounts[cls] <= 0) delete this.classCounts[cls]; this.typeCountsByClass[cls][template] -= 1; if (this.typeCountsByClass[cls][template] <= 0) delete this.typeCountsByClass[cls][template]; } } } } }; /** * Marks a technology as researched. * Note that this does not verify that the requirements are met. * * @param {string} tech - The technology to mark as researched. */ TechnologyManager.prototype.ResearchTechnology = function(tech) { this.StoppedResearch(tech, false); let modifiedComponents = {}; this.researchedTechs.add(tech); // Store the modifications in an easy to access structure. let template = TechnologyTemplates.Get(tech); if (template.modifications) { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("tech/" + tech, DeriveModificationsFromTech(template), this.entity); } if (template.replaces && template.replaces.length > 0) { for (let i of template.replaces) { if (!i || this.IsTechnologyResearched(i)) continue; this.researchedTechs.add(i); // Change the EntityLimit if any. let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined) { let playerID = cmpPlayer.GetPlayerID(); let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(i); } } } this.UpdateAutoResearch(); let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined) return; let playerID = cmpPlayer.GetPlayerID(); // Change the EntityLimit if any. let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(tech); // Always send research finished message. Engine.PostMessage(this.entity, MT_ResearchFinished, { "player": playerID, "tech": tech }); if (tech.startsWith("phase") && !template.autoResearch) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [playerID], "phaseName": tech, "phaseState": "completed" }); } }; /** * Marks a technology as being queued for research at the given entityID. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher) { this.researchQueued.set(tech, researcher); + + const cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + if (!cmpPlayer) + return; + const playerID = cmpPlayer.GetPlayerID(); + + Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).CallEvent("OnResearchQueued", { + "playerid": playerID, + "technologyTemplate": tech, + "researcherEntity": researcher + }); }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function(tech, notification) { this.researchStarted.add(tech); if (notification && tech.startsWith("phase")) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "started" }); } }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. */ TechnologyManager.prototype.StoppedResearch = function(tech, notification) { if (notification && tech.startsWith("phase") && this.researchStarted.has(tech)) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "aborted" }); } this.researchQueued.delete(tech); this.researchStarted.delete(tech); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; /** * Returns the names of technologies that are currently being researched (non-queued). */ TechnologyManager.prototype.GetStartedTechs = function() { return this.researchStarted; }; /** * Gets the entity currently researching the technology. */ TechnologyManager.prototype.GetResearcher = function(tech) { return this.researchQueued.get(tech); }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (revision 26000) @@ -0,0 +1,727 @@ +function Trainer() {} + +Trainer.prototype.Schema = + "Allows the entity to train new units." + + "" + + "0.7" + + "" + + "\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + + ""; + +/** + * This object represents a batch of entities being trained. + */ +Trainer.prototype.Item = function() {}; + +/** + * @param {string} templateName - The name of the template we ought to train. + * @param {number} count - The size of the batch to train. + * @param {number} trainer - The entity ID of our trainer. + * @param {string} metadata - Optionally any metadata to attach to us. + */ +Trainer.prototype.Item.prototype.Init = function(templateName, count, trainer, metadata) +{ + this.count = count; + this.templateName = templateName; + this.trainer = trainer; + this.metadata = metadata; +}; + +/** + * Prepare for the queue. + * @param {Object} trainCostMultiplier - The multipliers to use when calculating costs. + * @param {number} batchTimeMultiplier - The factor to use when training this batches. + * + * @return {boolean} - Whether the item was successfully initiated. + */ +Trainer.prototype.Item.prototype.Queue = function(trainCostMultiplier, batchTimeMultiplier) +{ + if (!Number.isInteger(this.count) || this.count <= 0) + { + error("Invalid batch count " + this.count + "."); + return false; + } + const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + const template = cmpTemplateManager.GetTemplate(this.templateName); + if (!template) + return false; + + const cmpPlayer = QueryOwnerInterface(this.trainer); + if (!cmpPlayer) + return false; + this.player = cmpPlayer.GetPlayerID(); + + this.resources = {}; + const totalResources = {}; + + for (const res in template.Cost.Resources) + { + this.resources[res] = (trainCostMultiplier[res] === undefined ? 1 : trainCostMultiplier[res]) * + ApplyValueModificationsToTemplate( + "Cost/Resources/" + res, + +template.Cost.Resources[res], + this.player, + template); + + totalResources[res] = Math.floor(this.count * this.resources[res]); + } + // TrySubtractResources should report error to player (they ran out of resources). + if (!cmpPlayer.TrySubtractResources(totalResources)) + return false; + + this.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, this.player, template); + + if (template.TrainingRestrictions) + { + const unitCategory = template.TrainingRestrictions.Category; + const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits); + if (cmpPlayerEntityLimits) + { + if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, this.count, this.templateName, template.TrainingRestrictions.MatchLimit)) + // Already warned, return. + { + cmpPlayer.RefundResources(totalResources); + return false; + } + // ToDo: Should warn here v and return? + cmpPlayerEntityLimits.ChangeCount(unitCategory, this.count); + if (template.TrainingRestrictions.MatchLimit) + cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, this.count); + } + } + + const buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, this.player, template); + + const time = batchTimeMultiplier * (trainCostMultiplier.time || 1) * buildTime * 1000; + this.timeRemaining = time; + this.timeTotal = time; + + const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + cmpTrigger.CallEvent("OnTrainingQueued", { + "playerid": this.player, + "unitTemplate": this.templateName, + "count": this.count, + "metadata": this.metadata, + "trainerEntity": this.trainer + }); + + return true; +}; + +/** + * Destroy cached entities, refund resources and free (population) limits. + */ +Trainer.prototype.Item.prototype.Stop = function() +{ + // Destroy any cached entities (those which didn't spawn for some reason). + if (this.entities?.length) + { + for (const ent of this.entities) + Engine.DestroyEntity(ent); + + delete this.entities; + } + + const cmpPlayer = QueryPlayerIDInterface(this.player); + + const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + const template = cmpTemplateManager.GetTemplate(this.templateName); + if (template.TrainingRestrictions) + { + const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits); + if (cmpPlayerEntityLimits) + cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -this.count); + if (template.TrainingRestrictions.MatchLimit) + cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, -this.count); + } + + const cmpStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker); + const totalCosts = {}; + for (const resource in this.resources) + { + totalCosts[resource] = Math.floor(this.count * this.resources[resource]); + if (cmpStatisticsTracker) + cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]); + } + + if (cmpPlayer) + { + if (this.started) + cmpPlayer.UnReservePopulationSlots(this.population * this.count); + cmpPlayer.RefundResources(totalCosts); + cmpPlayer.UnBlockTraining(); + } + + delete this.resources; +}; + +/** + * This starts the item, reserving population. + * @return {boolean} - Whether the item was started successfully. + */ +Trainer.prototype.Item.prototype.Start = function() +{ + const cmpPlayer = QueryPlayerIDInterface(this.player); + if (!cmpPlayer) + return false; + + const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName); + this.population = ApplyValueModificationsToTemplate( + "Cost/Population", + +template.Cost.Population, + this.player, + template); + + this.missingPopSpace = cmpPlayer.TryReservePopulationSlots(this.population * this.count); + if (this.missingPopSpace) + { + cmpPlayer.BlockTraining(); + return false; + } + cmpPlayer.UnBlockTraining(); + + Engine.PostMessage(this.trainer, MT_TrainingStarted, { "entity": this.trainer }); + + this.started = true; + return true; +}; + +Trainer.prototype.Item.prototype.Finish = function() +{ + this.Spawn(); + if (!this.count) + this.finished = true; +}; + +/* + * This function creates the entities and places them in world if possible + * (some of these entities may be garrisoned directly if autogarrison, the others are spawned). + */ +Trainer.prototype.Item.prototype.Spawn = function() +{ + const createdEnts = []; + const spawnedEnts = []; + + // We need entities to test spawning, but we don't want to waste resources, + // so only create them once and use as needed. + if (!this.entities) + { + this.entities = []; + for (let i = 0; i < this.count; ++i) + this.entities.push(Engine.AddEntity(this.templateName)); + } + + let autoGarrison; + const cmpRallyPoint = Engine.QueryInterface(this.trainer, IID_RallyPoint); + if (cmpRallyPoint) + { + const data = cmpRallyPoint.GetData()[0]; + if (data?.target && data.target == this.trainer && data.command == "garrison") + autoGarrison = true; + } + + const cmpFootprint = Engine.QueryInterface(this.trainer, IID_Footprint); + const cmpPosition = Engine.QueryInterface(this.trainer, IID_Position); + const positionTrainer = cmpPosition && cmpPosition.GetPosition(); + + const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits); + const cmpPlayerStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker); + while (this.entities.length) + { + const ent = this.entities[0]; + const cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); + let garrisoned = false; + + if (autoGarrison) + { + const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); + if (cmpGarrisonable) + { + // Temporary owner affectation needed for GarrisonHolder checks. + cmpNewOwnership.SetOwnerQuiet(this.player); + garrisoned = cmpGarrisonable.Garrison(this.trainer); + cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER); + } + } + + if (!garrisoned) + { + const pos = cmpFootprint.PickSpawnPoint(ent); + if (pos.y < 0) + break; + + const cmpNewPosition = Engine.QueryInterface(ent, IID_Position); + cmpNewPosition.JumpTo(pos.x, pos.z); + + if (positionTrainer) + cmpNewPosition.SetYRotation(positionTrainer.horizAngleTo(pos)); + + spawnedEnts.push(ent); + } + + // Decrement entity count in the EntityLimits component + // since it will be increased by EntityLimits.OnGlobalOwnershipChanged, + // i.e. we replace a 'trained' entity by 'alive' one. + // Must be done after spawn check so EntityLimits decrements only if unit spawns. + if (cmpPlayerEntityLimits) + { + const cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); + if (cmpTrainingRestrictions) + cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1); + } + cmpNewOwnership.SetOwner(this.player); + + if (cmpPlayerStatisticsTracker) + cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent); + + this.count--; + this.entities.shift(); + createdEnts.push(ent); + } + + if (spawnedEnts.length && !autoGarrison && cmpRallyPoint) + for (const com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts)) + ProcessCommand(this.player, com); + + const cmpPlayer = QueryOwnerInterface(this.trainer); + if (createdEnts.length) + { + if (this.population) + cmpPlayer.UnReservePopulationSlots(this.population * createdEnts.length); + // Play a sound, but only for the first in the batch (to avoid nasty phasing effects). + PlaySound("trained", createdEnts[0]); + Engine.PostMessage(this.trainer, MT_TrainingFinished, { + "entities": createdEnts, + "owner": this.player, + "metadata": this.metadata + }); + } + if (this.count) + { + cmpPlayer.BlockTraining(); + + if (!this.spawnNotified) + { + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ + "players": [cmpPlayer.GetPlayerID()], + "message": markForTranslation("Can't find free space to spawn trained units."), + "translateMessage": true + }); + this.spawnNotified = true; + } + } + else + { + cmpPlayer.UnBlockTraining(); + delete this.spawnNotified; + } +}; + +/** + * @param {number} allocatedTime - The time allocated to this item. + * @return {number} - The time used for this item. + */ +Trainer.prototype.Item.prototype.Progress = function(allocatedTime) +{ + // We couldn't start this timeout, try again later. + if (!this.started && !this.Start()) + return allocatedTime; + + if (this.timeRemaining > allocatedTime) + { + this.timeRemaining -= allocatedTime; + return allocatedTime; + } + this.Finish(); + return this.timeRemaining; +}; + +Trainer.prototype.Item.prototype.Pause = function() +{ + this.paused = true; +}; + +Trainer.prototype.Item.prototype.Unpause = function() +{ + delete this.paused; +}; + +/** + * @return {Object} - Some basic information of this batch. + */ +Trainer.prototype.Item.prototype.GetBasicInfo = function() +{ + return { + "unitTemplate": this.templateName, + "count": this.count, + "neededSlots": this.missingPopSpace, + "progress": 1 - (this.timeRemaining / this.timeTotal), + "timeRemaining": this.timeRemaining, + "paused": this.paused, + "metadata": this.metadata + }; +}; + +Trainer.prototype.Item.prototype.Serialize = function(id) +{ + return { + "id": id, + "count": this.count, + "entities": this.entities, + "metadata": this.metadata, + "missingPopSpace": this.missingPopSpace, + "paused": this.paused, + "player": this.player, + "trainer": this.trainer, + "resource": this.resources, + "started": this.started, + "templateName": this.templateName, + "timeRemaining": this.timeRemaining, + "timeTotal": this.timeTotal, + }; +}; + +Trainer.prototype.Item.prototype.Deserialize = function(data) +{ + this.Init(data.templateName, data.count, data.trainer, data.metadata); + + this.entities = data.entities; + this.missingPopSpace = data.missingPopSpace; + this.paused = data.paused; + this.player = data.player; + this.trainer = data.trainer; + this.resources = data.resources; + this.started = data.started; + this.timeRemaining = data.timeRemaining; + this.timeTotal = data.timeTotal; +}; + +Trainer.prototype.Init = function() +{ + this.nextID = 1; + this.queue = new Map(); +}; + +Trainer.prototype.Serialize = function() +{ + const queue = []; + for (const [id, item] of this.queue) + queue.push(item.Serialize(id)); + + return { + "entitiesMap": this.entitiesMap, + "nextID": this.nextID, + "queue": queue + }; +}; + +Trainer.prototype.Deserialize = function(data) +{ + this.Init(); + this.entitiesMap = data.entitiesMap; + this.nextID = data.nextID; + for (const item of data.queue) + { + const newItem = new this.Item(); + newItem.Deserialize(item); + this.queue.set(item.id, newItem); + } +}; + +/* + * Returns list of entities that can be trained by this entity. + */ +Trainer.prototype.GetEntitiesList = function() +{ + return Array.from(this.entitiesMap.values()); +}; + +/** + * Calculate the new list of producible entities + * and update any entities currently being produced. + */ +Trainer.prototype.CalculateEntitiesMap = function() +{ + // Don't reset the map, it's used below to update entities. + if (!this.entitiesMap) + this.entitiesMap = new Map(); + if (!this.template.Entities) + return; + + const string = this.template.Entities._string; + // Tokens can be added -> process an empty list to get them. + let addedTokens = ApplyValueModificationsToEntity("Trainer/Entities/_string", "", this.entity); + if (!addedTokens && !string) + return; + + addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/); + + const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + const cmpPlayer = QueryOwnerInterface(this.entity); + + const disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {}; + + /** + * Process tokens: + * - process token modifiers (this is a bit tricky). + * - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID + * - remove disabled entities + * - upgrade templates where necessary + * This also updates currently queued production (it's more convenient to do it here). + */ + + const removeAllQueuedTemplate = (token) => { + const queue = clone(this.queue); + const template = this.entitiesMap.get(token); + for (const [id, item] of queue) + if (item.templateName == template) + this.StopBatch(id); + }; + + // ToDo: Notice this doesn't account for entity limits changing due to the template change. + const updateAllQueuedTemplate = (token, updateTo) => { + const template = this.entitiesMap.get(token); + for (const [id, item] of this.queue) + if (item.templateName === template) + item.templateName = updateTo; + }; + + const toks = string.split(/\s+/); + for (const tok of addedTokens) + toks.push(tok); + + const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity)?.GetCiv(); + const playerCiv = cmpPlayer?.GetCiv(); + + const addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {}); + this.entitiesMap = toks.reduce((entMap, token) => { + const rawToken = token; + if (!(token in addedDict)) + { + // This is a bit wasteful but I can't think of a simpler/better way. + // The list of token is unlikely to be a performance bottleneck anyways. + token = ApplyValueModificationsToEntity("Trainer/Entities/_string", token, this.entity); + token = token.split(/\s+/); + if (token.every(tok => addedTokens.indexOf(tok) !== -1)) + { + removeAllQueuedTemplate(rawToken); + return entMap; + } + token = token[0]; + } + // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. + if (nativeCiv) + token = token.replace(/\{native\}/g, nativeCiv); + if (playerCiv) + token = token.replace(/\{civ\}/g, playerCiv); + + // Filter out disabled and invalid entities. + if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token)) + { + removeAllQueuedTemplate(rawToken); + return entMap; + } + + token = this.GetUpgradedTemplate(token); + entMap.set(rawToken, token); + updateAllQueuedTemplate(rawToken, token); + return entMap; + }, new Map()); +}; + +/* + * Returns the upgraded template name if necessary. + */ +Trainer.prototype.GetUpgradedTemplate = function(templateName) +{ + const cmpPlayer = QueryOwnerInterface(this.entity); + if (!cmpPlayer) + return templateName; + + const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + let template = cmpTemplateManager.GetTemplate(templateName); + while (template && template.Promotion !== undefined) + { + const requiredXp = ApplyValueModificationsToTemplate( + "Promotion/RequiredXp", + +template.Promotion.RequiredXp, + cmpPlayer.GetPlayerID(), + template); + if (requiredXp > 0) + break; + templateName = template.Promotion.Entity; + template = cmpTemplateManager.GetTemplate(templateName); + } + return templateName; +}; + +/** + * @return {Object} - The multipliers to change the costs of any training activity with. + */ +Trainer.prototype.GetTrainCostMultiplier = function() +{ + const trainCostMultiplier = {}; + for (const res in this.template.TrainCostMultiplier) + trainCostMultiplier[res] = ApplyValueModificationsToEntity( + "Trainer/TrainCostMultiplier/" + res, + +this.template.TrainCostMultiplier[res], + this.entity); + + return trainCostMultiplier; +}; + +/* + * Returns batch build time. + */ +Trainer.prototype.GetBatchTime = function(batchSize) +{ + // TODO: work out what equation we should use here. + return Math.pow(batchSize, ApplyValueModificationsToEntity( + "Trainer/BatchTimeModifier", + +(this.template.BatchTimeModifier || 1), + this.entity)); +}; + +/** + * @param {string} templateName - The template name to check. + * @return {boolean} - Whether we can train this template. + */ +Trainer.prototype.CanTrain = function(templateName) +{ + return this.GetEntitiesList().includes(templateName); +}; + +/** + * @param {string} templateName - The entity to queue. + * @param {number} count - The batch size. + * @param {string} metadata - Any metadata attached to the item. + * + * @return {number} - The ID of the item. -1 if the item could not be queued. + */ +Trainer.prototype.QueueBatch = function(templateName, count, metadata) +{ + const item = new this.Item(); + item.Init(templateName, count, this.entity, metadata); + + const trainCostMultiplier = this.GetTrainCostMultiplier(); + const batchTimeMultiplier = this.GetBatchTime(count); + if (!item.Queue(trainCostMultiplier, batchTimeMultiplier)) + return -1; + + const id = this.nextID++; + this.queue.set(id, item); + return id; +}; + +/** + * @param {number} id - The ID of the batch being trained here we need to stop. + */ +Trainer.prototype.StopBatch = function(id) +{ + this.queue.get(id).Stop(); + this.queue.delete(id); +}; + +/** + * @param {number} id - The ID of the training. + */ +Trainer.prototype.PauseBatch = function(id) +{ + this.queue.get(id).Pause(); +}; + +/** + * @param {number} id - The ID of the training. + */ +Trainer.prototype.UnpauseBatch = function(id) +{ + this.queue.get(id).Unpause(); +}; + +/** + * @param {number} id - The ID of the batch to check. + * @return {boolean} - Whether we are currently training the batch. + */ +Trainer.prototype.HasBatch = function(id) +{ + return this.queue.has(id); +}; + +/** + * @parameter {number} id - The id of the training. + * @return {Object} - Some basic information about the training. + */ +Trainer.prototype.GetBatch = function(id) +{ + const item = this.queue.get(id); + return item?.GetBasicInfo(); +}; + +/** + * @param {number} id - The ID of the item we spent time on. + * @param {number} allocatedTime - The time we spent on the given item. + * @return {number} - The time we've actually used. + */ +Trainer.prototype.Progress = function(id, allocatedTime) +{ + const item = this.queue.get(id); + const usedTime = item.Progress(allocatedTime); + if (item.finished) + this.queue.delete(id); + return usedTime; +}; + +Trainer.prototype.OnCivChanged = function() +{ + this.CalculateEntitiesMap(); +}; + +Trainer.prototype.OnOwnershipChanged = function(msg) +{ + if (msg.to != INVALID_PLAYER) + this.CalculateEntitiesMap(); +}; + +Trainer.prototype.OnValueModification = function(msg) +{ + // If the promotion requirements of units is changed, + // update the entities list so that automatically promoted units are shown + // appropriately in the list. + if (msg.component != "Promotion" && (msg.component != "Trainer" || + !msg.valueNames.some(val => val.startsWith("Trainer/Entities/")))) + return; + + if (msg.entities.indexOf(this.entity) === -1) + return; + + // This also updates the queued production if necessary. + this.CalculateEntitiesMap(); + + // Inform the GUI that it'll need to recompute the selection panel. + // TODO: it would be better to only send the message if something actually changing + // for the current training queue. + const cmpPlayer = QueryOwnerInterface(this.entity); + if (cmpPlayer) + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); +}; + +Trainer.prototype.OnDisabledTemplatesChanged = function(msg) +{ + this.CalculateEntitiesMap(); +}; + +Engine.RegisterComponentType(IID_Trainer, "Trainer", Trainer); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/plain \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js (revision 26000) @@ -1,19 +1,7 @@ Engine.RegisterInterface("ProductionQueue"); /** * Message of the form {} * sent from ProductionQueue component to the current entity whenever the training queue changes. */ Engine.RegisterMessageType("ProductionQueueChanged"); - -/** - * Message of the form { "entity": number } - * sent from ProductionQueue component to the current entity whenever a unit is about to be trained. - */ -Engine.RegisterMessageType("TrainingStarted"); - -/** - * Message of the form { "entities": number[], "owner": number, "metadata": object } - * sent from ProductionQueue component to the current entity whenever a unit has been trained. - */ -Engine.RegisterMessageType("TrainingFinished"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js (revision 26000) @@ -0,0 +1 @@ +Engine.RegisterInterface("Researcher"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Researcher.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/plain \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js (revision 26000) @@ -0,0 +1,13 @@ +Engine.RegisterInterface("Trainer"); + +/** + * Message of the form { "entity": number } + * sent from Trainer component to the current entity whenever a unit is about to be trained. + */ +Engine.RegisterMessageType("TrainingStarted"); + +/** + * Message of the form { "entities": number[], "owner": number, "metadata": object } + * sent from Trainer component to the current entity whenever a unit has been trained. + */ +Engine.RegisterMessageType("TrainingFinished"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Trainer.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/plain \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 26000) @@ -1,617 +1,619 @@ Engine.LoadHelperScript("ObstructionSnap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/AlertRaiser.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Barter.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); -Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Gate.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Market.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/Population.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); +Engine.LoadComponentScript("interfaces/Researcher.js"); +Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/ResourceDropsite.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/ResourceTrickle.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Trader.js"); +Engine.LoadComponentScript("interfaces/Trainer.js"); Engine.LoadComponentScript("interfaces/TurretHolder.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/Treasure.js"); Engine.LoadComponentScript("interfaces/TreasureCollector.js"); Engine.LoadComponentScript("interfaces/Turretable.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("interfaces/Upkeep.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("GuiInterface.js"); Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetNames": () => ({ "food": "Food", "metal": "Metal", "stone": "Stone", "wood": "Wood" }), "GetResource": resource => ({ "aiAnalysisInfluenceGroup": resource == "food" ? "ignore" : resource == "wood" ? "abundant" : "sparse" }) }; var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); AddMock(SYSTEM_ENTITY, IID_Barter, { "GetPrices": function() { return { "buy": { "food": 150 }, "sell": { "food": 25 } }; } }); AddMock(SYSTEM_ENTITY, IID_EndGameManager, { "GetVictoryConditions": () => ["conquest", "wonder"], "GetAlliedVictory": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": function() { return 2; }, "GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; }, "GetMaxWorldPopulation": function() {} }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "GetLosVisibility": function(ent, player) { return "visible"; }, "GetLosCircular": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": function(ent) { return "example"; }, "GetTemplate": function(name) { return ""; } }); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetTime": function() { return 0; }, "SetTimeout": function(ent, iid, funcname, time, data) { return 0; } }); AddMock(100, IID_Player, { "GetName": function() { return "Player 1"; }, "GetCiv": function() { return "gaia"; }, "GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; }, "CanControlAllUnits": function() { return false; }, "GetPopulationCount": function() { return 10; }, "GetPopulationLimit": function() { return 20; }, "GetMaxPopulation": function() { return 200; }, "GetResourceCounts": function() { return { "food": 100 }; }, "GetResourceGatherers": function() { return { "food": 1 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, "GetTeam": function() { return -1; }, "GetLockTeams": function() { return false; }, "GetCheatsEnabled": function() { return false; }, "GetDiplomacy": function() { return [-1, 1]; }, "IsAlly": function() { return false; }, "IsMutualAlly": function() { return false; }, "IsNeutral": function() { return false; }, "IsEnemy": function() { return true; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, "GetSpyCostMultiplier": function() { return 1; }, "HasSharedDropsites": function() { return false; }, "HasSharedLos": function() { return false; } }); AddMock(100, IID_EntityLimits, { "GetLimits": function() { return { "Foo": 10 }; }, "GetCounts": function() { return { "Foo": 5 }; }, "GetLimitChangers": function() { return { "Foo": {} }; }, "GetMatchCounts": function() { return { "Bar": 0 }; } }); AddMock(100, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(100, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); AddMock(101, IID_Player, { "GetName": function() { return "Player 2"; }, "GetCiv": function() { return "mace"; }, "GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; }, "CanControlAllUnits": function() { return true; }, "GetPopulationCount": function() { return 40; }, "GetPopulationLimit": function() { return 30; }, "GetMaxPopulation": function() { return 300; }, "GetResourceCounts": function() { return { "food": 200 }; }, "GetResourceGatherers": function() { return { "food": 3 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, "GetTeam": function() { return -1; }, "GetLockTeams": function() {return false; }, "GetCheatsEnabled": function() { return false; }, "GetDiplomacy": function() { return [-1, 1]; }, "IsAlly": function() { return true; }, "IsMutualAlly": function() {return false; }, "IsNeutral": function() { return false; }, "IsEnemy": function() { return false; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, "GetSpyCostMultiplier": function() { return 1; }, "HasSharedDropsites": function() { return false; }, "HasSharedLos": function() { return false; } }); AddMock(101, IID_EntityLimits, { "GetLimits": function() { return { "Bar": 20 }; }, "GetCounts": function() { return { "Bar": 0 }; }, "GetLimitChangers": function() { return { "Bar": {} }; }, "GetMatchCounts": function() { return { "Foo": 0 }; } }); AddMock(101, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(101, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); AddMock(10, IID_Builder, { "GetEntitiesList": function() { return ["test1", "test2"]; }, }); AddMock(10, IID_Health, { "GetHitpoints": function() { return 50; }, "GetMaxHitpoints": function() { return 60; }, "IsRepairable": function() { return false; }, "IsUnhealable": function() { return false; } }); AddMock(10, IID_Identity, { "GetClassesList": function() { return ["class1", "class2"]; }, "GetRank": function() { return "foo"; }, "GetSelectionGroupName": function() { return "Selection Group Name"; }, "HasClass": function() { return true; }, "IsUndeletable": function() { return false; }, "IsControllable": function() { return true; }, "HasSomeFormation": function() { return false; }, "GetFormationsList": function() { return []; }, }); AddMock(10, IID_Position, { "GetTurretParent": function() { return INVALID_ENTITY; }, "GetPosition": function() { return { "x": 1, "y": 2, "z": 3 }; }, "IsInWorld": function() { return true; } }); AddMock(10, IID_ResourceTrickle, { "GetInterval": () => 1250, "GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 }) }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), { "id": 10, "player": INVALID_PLAYER, "template": "example", "identity": { "rank": "foo", "classes": ["class1", "class2"], "selectionGroupName": "Selection Group Name", "canDelete": true, "hasSomeFormation": false, "formations": [], "controllable": true, }, "position": { "x": 1, "y": 2, "z": 3 }, "hitpoints": 50, "maxHitpoints": 60, "needsRepair": false, "needsHeal": true, "builder": true, "visibility": "visible", "isBarterMarket": true, "resourceTrickle": { "interval": 1250, "rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 } } }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 26000) @@ -1,626 +1,83 @@ Engine.LoadHelperScript("Player.js"); -Engine.LoadHelperScript("Sound.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); -Engine.LoadComponentScript("interfaces/BuildRestrictions.js"); -Engine.LoadComponentScript("interfaces/EntityLimits.js"); -Engine.LoadComponentScript("interfaces/Foundation.js"); -Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); +Engine.LoadComponentScript("interfaces/Researcher.js"); Engine.LoadComponentScript("interfaces/Timer.js"); -Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); -Engine.LoadComponentScript("interfaces/Trigger.js"); +Engine.LoadComponentScript("interfaces/Trainer.js"); Engine.LoadComponentScript("interfaces/Upgrade.js"); -Engine.LoadComponentScript("EntityLimits.js"); Engine.LoadComponentScript("Timer.js"); -Engine.RegisterGlobal("Resources", { - "BuildSchema": (a, b) => {} -}); Engine.LoadComponentScript("ProductionQueue.js"); -Engine.LoadComponentScript("TrainingRestrictions.js"); -Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); -Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value); +const playerEnt = 2; +const playerID = 1; +const testEntity = 3; + +AddMock(SYSTEM_ENTITY, IID_Timer, { + "CancelTimer": (id) => {}, + "SetInterval": (ent, iid, func) => 1 +}); + +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => playerEnt +}); + +AddMock(playerEnt, IID_Player, { + "GetPlayerID": () => playerID +}); + +AddMock(testEntity, IID_Ownership, { + "GetOwner": () => playerID +}); + +AddMock(testEntity, IID_Trainer, { + "GetBatch": (id) => ({}), + "HasBatch": (id) => false, // Assume we've finished. + "Progress": (time) => time, + "QueueBatch": () => 1, + "StopBatch": (id) => {} +}); + +const cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", null); + + +// Test autoqueue. +cmpProdQueue.EnableAutoQueue(); + +cmpProdQueue.AddItem("some_template", "unit", 3); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); +cmpProdQueue.ProgressTimeout(null, 0); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); + +cmpProdQueue.RemoveItem(cmpProdQueue.nextID -1); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0); + +cmpProdQueue.DisableAutoQueue(); + + +// Test items which don't use all the time. +AddMock(testEntity, IID_Trainer, { + "GetBatch": (id) => ({}), + "HasBatch": (id) => false, // Assume we've finished. + "PauseBatch": (id) => {}, + "Progress": (time) => time - 250, + "QueueBatch": () => 1, + "StopBatch": (id) => {}, + "UnpauseBatch": (id) => {} +}); + +cmpProdQueue.AddItem("some_template", "unit", 2); +cmpProdQueue.AddItem("some_template", "unit", 3); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2); +cmpProdQueue.ProgressTimeout(null, 0); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0); + + +// Test pushing an item to the front. +cmpProdQueue.AddItem("some_template", "unit", 2); +cmpProdQueue.AddItem("some_template", "unit", 3, null, true); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue()[0].id, cmpProdQueue.nextID - 1); +TS_ASSERT(cmpProdQueue.GetQueue()[1].paused); -function testEntitiesList() -{ - Engine.RegisterGlobal("TechnologyTemplates", { - "Has": name => name == "phase_town_athen" || name == "phase_city_athen", - "Get": () => ({}) - }); - - const productionQueueId = 6; - const playerId = 1; - const playerEntityID = 2; - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({}) - }); - - let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", { - "Entities": { "_string": "units/{civ}/cavalry_javelineer_b " + - "units/{civ}/infantry_swordsman_b " + - "units/{native}/support_female_citizen" }, - "Technologies": { "_string": "gather_fishing_net " + - "phase_town_{civ} " + - "phase_city_{civ}" } - }); - cmpProductionQueue.GetUpgradedTemplate = (template) => template; - - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEntityID - }); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({}), - "GetPlayerID": () => playerId - }); - - AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, - "IsInProgress": () => false, - "IsTechnologyResearched": () => false - }); - - AddMock(productionQueueId, IID_Ownership, { - "GetOwner": () => playerId - }); - - AddMock(productionQueueId, IID_Identity, { - "GetCiv": () => "iber" - }); - - AddMock(productionQueueId, IID_Upgrade, { - "IsUpgrading": () => false - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] - ); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] - ); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": name => name == "units/iber/support_female_citizen", - "GetTemplate": name => ({}) - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber/support_female_citizen"]); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({}) - }); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }), - "GetPlayerID": () => playerId - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] - ); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }), - "GetPlayerID": () => playerId - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber/cavalry_javelineer_b", "units/iber/support_female_citizen"] - ); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "athen", - "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }), - "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }), - "GetPlayerID": () => playerId - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/athen/cavalry_javelineer_b", "units/iber/support_female_citizen"] - ); - TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), ["phase_town_athen", - "phase_city_athen"] - ); - - AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, - "IsInProgress": () => false, - "IsTechnologyResearched": tech => tech == "phase_town_athen" - }); - TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), [undefined, "phase_city_athen"]); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetPlayerID": () => playerId - }); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetTechnologiesList(), - ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] - ); -} - -function regression_test_d1879() -{ - // Setup - let playerEnt = 2; - let playerID = 1; - let testEntity = 3; - let spawedEntityIDs = [4, 5, 6, 7, 8]; - let spawned = 0; - - Engine.AddEntity = () => { - let id = spawedEntityIDs[spawned++]; - - ConstructComponent(id, "TrainingRestrictions", { - "Category": "some_limit" - }); - - AddMock(id, IID_Identity, { - "GetClassesList": () => [] - }); - - AddMock(id, IID_Position, { - "JumpTo": () => {} - }); - - AddMock(id, IID_Ownership, { - "SetOwner": (pid) => { - let cmpEntLimits = QueryOwnerInterface(id, IID_EntityLimits); - cmpEntLimits.OnGlobalOwnershipChanged({ - "entity": id, - "from": -1, - "to": pid - }); - }, - "GetOwner": () => playerID - }); - - return id; - }; - - ConstructComponent(playerEnt, "EntityLimits", { - "Limits": { - "some_limit": 8 - }, - "LimitChangers": {}, - "LimitRemovers": {} - }); - - AddMock(SYSTEM_ENTITY, IID_GuiInterface, { - "PushNotification": () => {} - }); - - AddMock(SYSTEM_ENTITY, IID_Trigger, { - "CallEvent": () => {} - }); - - AddMock(SYSTEM_ENTITY, IID_Timer, { - "SetInterval": (ent, iid, func) => 1, - "CancelTimer": (id) => {} - }); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({ - "Cost": { - "BuildTime": 0, - "Population": 1, - "Resources": {} - }, - "TrainingRestrictions": { - "Category": "some_limit", - "MatchLimit": "7" - } - }) - }); - - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEnt - }); - - AddMock(playerEnt, IID_Player, { - "GetCiv": () => "iber", - "GetPlayerID": () => playerID, - "GetTimeMultiplier": () => 0, - "BlockTraining": () => {}, - "UnBlockTraining": () => {}, - "UnReservePopulationSlots": () => {}, - "TrySubtractResources": () => true, - "AddResources": () => true, - "TryReservePopulationSlots": () => false // Always have pop space. - }); - - AddMock(testEntity, IID_Ownership, { - "GetOwner": () => playerID - }); - - let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", { - "Entities": { "_string": "some_template" }, - "BatchTimeModifier": 1 - }); - - let cmpEntLimits = QueryOwnerInterface(testEntity, IID_EntityLimits); - TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 8)); - TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 9)); - TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5, "some_template", 8)); - TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 10, "some_template", 8)); - - // Check that the entity limits do get updated if the spawn succeeds. - AddMock(testEntity, IID_Footprint, { - "PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 }) - }); - - cmpProdQueue.AddItem("some_template", "unit", 3); - - TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); - TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); - - cmpProdQueue.ProgressTimeout(null, 0); - - TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); - TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); - - // Now check that it doesn't get updated when the spawn doesn't succeed. - AddMock(testEntity, IID_Footprint, { - "PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 }) - }); - - AddMock(testEntity, IID_Upgrade, { - "IsUpgrading": () => false - }); - - cmpProdQueue.AddItem("some_template", "unit", 3); - cmpProdQueue.ProgressTimeout(null, 0); - - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); - TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6); - TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 6); - - // Check that when the batch is removed the counts are subtracted again. - cmpProdQueue.RemoveItem(cmpProdQueue.GetQueue()[0].id); - TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); - TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); -} - -function test_batch_adding() -{ - let playerEnt = 2; - let playerID = 1; - let testEntity = 3; - - ConstructComponent(playerEnt, "EntityLimits", { - "Limits": { - "some_limit": 8 - }, - "LimitChangers": {}, - "LimitRemovers": {} - }); - - AddMock(SYSTEM_ENTITY, IID_GuiInterface, { - "PushNotification": () => {} - }); - - AddMock(SYSTEM_ENTITY, IID_Trigger, { - "CallEvent": () => {} - }); - - AddMock(SYSTEM_ENTITY, IID_Timer, { - "SetInterval": (ent, iid, func) => 1 - }); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({ - "Cost": { - "BuildTime": 0, - "Population": 1, - "Resources": {} - }, - "TrainingRestrictions": { - "Category": "some_limit" - } - }) - }); - - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEnt - }); - - AddMock(playerEnt, IID_Player, { - "GetCiv": () => "iber", - "GetPlayerID": () => playerID, - "GetTimeMultiplier": () => 0, - "BlockTraining": () => {}, - "UnBlockTraining": () => {}, - "UnReservePopulationSlots": () => {}, - "TrySubtractResources": () => true, - "TryReservePopulationSlots": () => false // Always have pop space. - }); - - AddMock(testEntity, IID_Ownership, { - "GetOwner": () => playerID - }); - - let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", { - "Entities": { "_string": "some_template" }, - "BatchTimeModifier": 1 - }); - - - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0); - AddMock(testEntity, IID_Upgrade, { - "IsUpgrading": () => true - }); - - cmpProdQueue.AddItem("some_template", "unit", 3); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0); - - AddMock(testEntity, IID_Upgrade, { - "IsUpgrading": () => false - }); - - cmpProdQueue.AddItem("some_template", "unit", 3); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); -} - -function test_batch_removal() -{ - let playerEnt = 2; - let playerID = 1; - let testEntity = 3; - - ConstructComponent(playerEnt, "EntityLimits", { - "Limits": { - "some_limit": 8 - }, - "LimitChangers": {}, - "LimitRemovers": {} - }); - - AddMock(SYSTEM_ENTITY, IID_GuiInterface, { - "PushNotification": () => {} - }); - - AddMock(SYSTEM_ENTITY, IID_Trigger, { - "CallEvent": () => {} - }); - - let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", null); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({ - "Cost": { - "BuildTime": 0, - "Population": 1, - "Resources": {} - }, - "TrainingRestrictions": { - "Category": "some_limit" - } - }) - }); - - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEnt - }); - - let cmpPlayer = AddMock(playerEnt, IID_Player, { - "GetCiv": () => "iber", - "GetPlayerID": () => playerID, - "GetTimeMultiplier": () => 0, - "BlockTraining": () => {}, - "UnBlockTraining": () => {}, - "UnReservePopulationSlots": () => {}, - "TrySubtractResources": () => true, - "AddResources": () => {}, - "TryReservePopulationSlots": () => 1 - }); - let cmpPlayerBlockSpy = new Spy(cmpPlayer, "BlockTraining"); - let cmpPlayerUnblockSpy = new Spy(cmpPlayer, "UnBlockTraining"); - - AddMock(testEntity, IID_Ownership, { - "GetOwner": () => playerID - }); - - let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", { - "Entities": { "_string": "some_template" }, - "BatchTimeModifier": 1 - }); - - cmpProdQueue.AddItem("some_template", "unit", 3); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); - cmpTimer.OnUpdate({ "turnLength": 1 }); - TS_ASSERT_EQUALS(cmpPlayerBlockSpy._called, 1); - - cmpProdQueue.AddItem("some_template", "unit", 2); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2); - - cmpProdQueue.RemoveItem(1); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); - TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 1); - - cmpProdQueue.RemoveItem(2); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0); - cmpTimer.OnUpdate({ "turnLength": 1 }); - TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 2); - - cmpProdQueue.AddItem("some_template", "unit", 3); - cmpProdQueue.AddItem("some_template", "unit", 3); - cmpPlayer.TryReservePopulationSlots = () => false; - cmpProdQueue.RemoveItem(3); - TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 3); - cmpTimer.OnUpdate({ "turnLength": 1 }); - TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 4); -} - -function test_token_changes() -{ - const ent = 10; - let cmpProductionQueue = ConstructComponent(10, "ProductionQueue", { - "Entities": { "_string": "units/{civ}/a " + - "units/{civ}/b" }, - "Technologies": { "_string": "a " + - "b_{civ} " + - "c_{civ}" }, - "BatchTimeModifier": 1 - }); - cmpProductionQueue.GetUpgradedTemplate = (template) => template; - - // Merges interface of multiple components because it's enough here. - Engine.RegisterGlobal("QueryOwnerInterface", () => ({ - // player - "GetCiv": () => "test", - "GetDisabledTemplates": () => [], - "GetDisabledTechnologies": () => [], - "TryReservePopulationSlots": () => false, // Always have pop space. - "TrySubtractResources": () => true, - "UnBlockTraining": () => {}, - "AddResources": () => {}, - "GetPlayerID": () => 1, - // entitylimits - "ChangeCount": () => {}, - "AllowedToTrain": () => true, - // techmanager - "CheckTechnologyRequirements": () => true, - "IsTechnologyResearched": () => false, - "IsInProgress": () => false - })); - Engine.RegisterGlobal("QueryPlayerIDInterface", QueryOwnerInterface); - - AddMock(SYSTEM_ENTITY, IID_GuiInterface, { - "SetSelectionDirty": () => {} - }); - - // Test Setup - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), ["units/test/a", "units/test/b"] - ); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetTechnologiesList(), ["a", "b_generic", "c_generic"] - ); - // Add a unit of each type to our queue, validate. - cmpProductionQueue.AddItem("units/test/a", "unit", 1, {}); - cmpProductionQueue.AddItem("units/test/b", "unit", 1, {}); - TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test/a"); - TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[1].unitTemplate, "units/test/b"); - - // Add a modifier that replaces unit A with unit C, - // adds a unit D and removes unit B from the roster. - Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => { - return HandleTokens(val, "units/{civ}/a>units/{civ}/c units/{civ}/d -units/{civ}/b"); - }); - - cmpProductionQueue.OnValueModification({ - "component": "ProductionQueue", - "valueNames": ["ProductionQueue/Entities/_string"], - "entities": [ent] - }); - - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), ["units/test/c", "units/test/d"] - ); - TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test/c"); - TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue().length, 1); -} - -function test_auto_queue() -{ - let playerEnt = 2; - let playerID = 1; - let testEntity = 3; - - ConstructComponent(playerEnt, "EntityLimits", { - "Limits": { - "some_limit": 8 - }, - "LimitChangers": {}, - "LimitRemovers": {} - }); - - AddMock(SYSTEM_ENTITY, IID_GuiInterface, { - "PushNotification": () => {} - }); - - AddMock(SYSTEM_ENTITY, IID_Trigger, { - "CallEvent": () => {} - }); - - AddMock(SYSTEM_ENTITY, IID_Timer, { - "SetInterval": (ent, iid, func) => 1 - }); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({ - "Cost": { - "BuildTime": 0, - "Population": 1, - "Resources": {} - }, - "TrainingRestrictions": { - "Category": "some_limit" - } - }) - }); - - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEnt - }); - - AddMock(playerEnt, IID_Player, { - "GetCiv": () => "iber", - "GetPlayerID": () => playerID, - "GetTimeMultiplier": () => 0, - "BlockTraining": () => {}, - "UnBlockTraining": () => {}, - "UnReservePopulationSlots": () => {}, - "TrySubtractResources": () => true, - "TryReservePopulationSlots": () => false // Always have pop space. - }); - - AddMock(testEntity, IID_Ownership, { - "GetOwner": () => playerID - }); - - let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", { - "Entities": { "_string": "some_template" }, - "BatchTimeModifier": 1 - }); - - cmpProdQueue.EnableAutoQueue(); - - cmpProdQueue.AddItem("some_template", "unit", 3); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); - cmpProdQueue.ProgressTimeout(null, 0); - TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); -} - -testEntitiesList(); -regression_test_d1879(); -test_batch_adding(); -test_batch_removal(); -test_auto_queue(); -test_token_changes(); +cmpProdQueue.ProgressTimeout(null, 0); +TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js (revision 26000) @@ -0,0 +1,153 @@ +Engine.RegisterGlobal("Resources", { + "BuildSchema": (a, b) => {} +}); +Engine.LoadHelperScript("Player.js"); +Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/Researcher.js"); +Engine.LoadComponentScript("Researcher.js"); + +Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); + +const playerID = 1; +const playerEntityID = 11; +const entityID = 21; + +Engine.RegisterGlobal("TechnologyTemplates", { + "Has": name => name == "phase_town_athen" || name == "phase_city_athen", + "Get": () => ({}) +}); + +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => playerEntityID +}); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}) +}); + +AddMock(playerEntityID, IID_TechnologyManager, { + "CheckTechnologyRequirements": () => true, + "IsInProgress": () => false, + "IsTechnologyResearched": () => false +}); + +AddMock(entityID, IID_Ownership, { + "GetOwner": () => playerID +}); + +AddMock(entityID, IID_Identity, { + "GetCiv": () => "iber" +}); + +const cmpResearcher = ConstructComponent(entityID, "Researcher", { + "Technologies": { "_string": "gather_fishing_net " + + "phase_town_{civ} " + + "phase_city_{civ}" } +}); + +TS_ASSERT_UNEVAL_EQUALS( + cmpResearcher.GetTechnologiesList(), + ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] +); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "athen", + "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }) +}); +TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), ["phase_town_athen", "phase_city_athen"]); + +AddMock(playerEntityID, IID_TechnologyManager, { + "CheckTechnologyRequirements": () => true, + "IsInProgress": () => false, + "IsTechnologyResearched": tech => tech == "phase_town_athen" +}); +TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.GetTechnologiesList(), [undefined, "phase_city_athen"]); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}) +}); +TS_ASSERT_UNEVAL_EQUALS( + cmpResearcher.GetTechnologiesList(), + ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] +); + +Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value + " some_test"); +TS_ASSERT_UNEVAL_EQUALS( + cmpResearcher.GetTechnologiesList(), + ["gather_fishing_net", "phase_town_generic", "phase_city_generic", "some_test"] +); + + +// Test Queuing a tech. +const queuedTech = "gather_fishing_net"; +const cost = { + "food": 10 +}; +Engine.RegisterGlobal("TechnologyTemplates", { + "Has": () => true, + "Get": () => ({ + "cost": cost, + "researchTime": 1 + }) +}); + +const cmpPlayer = AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTechnologies": () => ({}), + "GetPlayerID": () => playerID, + "TrySubtractResources": (resources) => { + TS_ASSERT_UNEVAL_EQUALS(resources, cost); + // Just have enough resources. + return true; + }, + "RefundResources": (resources) => { + TS_ASSERT_UNEVAL_EQUALS(resources, cost); + }, +}); +let spyCmpPlayer = new Spy(cmpPlayer, "TrySubtractResources"); +const techManager = AddMock(playerEntityID, IID_TechnologyManager, { + "CheckTechnologyRequirements": () => true, + "IsInProgress": () => false, + "IsTechnologyResearched": () => false, + "QueuedResearch": (templateName, researcher) => { + TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); + TS_ASSERT_UNEVAL_EQUALS(researcher, entityID); + }, + "StoppedResearch": (templateName, _) => { + TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); + }, + "StartedResearch": (templateName, _) => { + TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); + }, + "ResearchTechnology": (templateName, _) => { + TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech); + } +}); +let spyTechManager = new Spy(techManager, "QueuedResearch"); +let id = cmpResearcher.QueueTechnology(queuedTech); +TS_ASSERT_EQUALS(spyTechManager._called, 1); +TS_ASSERT_EQUALS(spyCmpPlayer._called, 1); +TS_ASSERT_EQUALS(cmpResearcher.queue.size, 1); + + +// Test removing a queued tech. +spyCmpPlayer = new Spy(cmpPlayer, "RefundResources"); +spyTechManager = new Spy(techManager, "StoppedResearch"); +cmpResearcher.StopResearching(id); +TS_ASSERT_EQUALS(spyTechManager._called, 1); +TS_ASSERT_EQUALS(spyCmpPlayer._called, 1); +TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0); + + +// Test finishing a queued tech. +id = cmpResearcher.QueueTechnology(queuedTech); +TS_ASSERT_EQUALS(cmpResearcher.GetResearchingTechnology(id).progress, 0); +TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 500), 500); +TS_ASSERT_EQUALS(cmpResearcher.GetResearchingTechnology(id).progress, 0.5); + +spyTechManager = new Spy(techManager, "ResearchTechnology"); +TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 1000), 500); +TS_ASSERT_EQUALS(spyTechManager._called, 1); +TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Researcher.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/plain \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js (revision 26000) @@ -0,0 +1,301 @@ +Engine.RegisterGlobal("Resources", { + "BuildSchema": (a, b) => {} +}); +Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("Sound.js"); +Engine.LoadComponentScript("interfaces/BuildRestrictions.js"); +Engine.LoadComponentScript("interfaces/EntityLimits.js"); +Engine.LoadComponentScript("interfaces/Foundation.js"); +Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); +Engine.LoadComponentScript("interfaces/Trainer.js"); +Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); +Engine.LoadComponentScript("interfaces/Trigger.js"); +Engine.LoadComponentScript("EntityLimits.js"); +Engine.LoadComponentScript("Trainer.js"); +Engine.LoadComponentScript("TrainingRestrictions.js"); + +Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value); +Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value); + +const playerID = 1; +const playerEntityID = 11; +const entityID = 21; + +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "GetTemplate": name => ({}) +}); + +const cmpTrainer = ConstructComponent(entityID, "Trainer", { + "Entities": { "_string": "units/{civ}/cavalry_javelineer_b " + + "units/{civ}/infantry_swordsman_b " + + "units/{native}/support_female_citizen" } +}); +cmpTrainer.GetUpgradedTemplate = (template) => template; + +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => playerEntityID +}); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTemplates": () => ({}), + "GetPlayerID": () => playerID +}); + +AddMock(entityID, IID_Ownership, { + "GetOwner": () => playerID +}); + +AddMock(entityID, IID_Identity, { + "GetCiv": () => "iber" +}); + +cmpTrainer.CalculateEntitiesMap(); +TS_ASSERT_UNEVAL_EQUALS( + cmpTrainer.GetEntitiesList(), + ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] +); + +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": name => name == "units/iber/support_female_citizen", + "GetTemplate": name => ({}) +}); + +cmpTrainer.CalculateEntitiesMap(); +TS_ASSERT_UNEVAL_EQUALS(cmpTrainer.GetEntitiesList(), ["units/iber/support_female_citizen"]); + +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "GetTemplate": name => ({}) +}); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }), + "GetPlayerID": () => playerID +}); + +cmpTrainer.CalculateEntitiesMap(); +TS_ASSERT_UNEVAL_EQUALS( + cmpTrainer.GetEntitiesList(), + ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] +); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }), + "GetPlayerID": () => playerID +}); + +cmpTrainer.CalculateEntitiesMap(); +TS_ASSERT_UNEVAL_EQUALS( + cmpTrainer.GetEntitiesList(), + ["units/iber/cavalry_javelineer_b", "units/iber/support_female_citizen"] +); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "athen", + "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }), + "GetPlayerID": () => playerID +}); + +cmpTrainer.CalculateEntitiesMap(); +TS_ASSERT_UNEVAL_EQUALS( + cmpTrainer.GetEntitiesList(), + ["units/athen/cavalry_javelineer_b", "units/iber/support_female_citizen"] +); + +AddMock(playerEntityID, IID_Player, { + "GetCiv": () => "iber", + "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": false }), + "GetPlayerID": () => playerID +}); + +cmpTrainer.CalculateEntitiesMap(); +TS_ASSERT_UNEVAL_EQUALS( + cmpTrainer.GetEntitiesList(), + ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] +); + + +// Test Queuing a unit. +const queuedUnit = "units/iber/infantry_swordsman_b"; +const cost = { + "food": 10 +}; + +AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "GetTemplate": name => ({ + "Cost": { + "BuildTime": 1, + "Population": 1, + "Resources": cost + }, + "TrainingRestrictions": { + "Category": "some_limit", + "MatchLimit": "7" + } + }) +}); +AddMock(SYSTEM_ENTITY, IID_Trigger, { + "CallEvent": () => {} +}); +AddMock(SYSTEM_ENTITY, IID_GuiInterface, { + "PushNotification": () => {}, + "SetSelectionDirty": () => {} +}); + +const cmpPlayer = AddMock(playerEntityID, IID_Player, { + "BlockTraining": () => {}, + "GetCiv": () => "iber", + "GetPlayerID": () => playerID, + "RefundResources": (resources) => { + TS_ASSERT_UNEVAL_EQUALS(resources, cost); + }, + "TrySubtractResources": (resources) => { + TS_ASSERT_UNEVAL_EQUALS(resources, cost); + // Just have enough resources. + return true; + }, + "TryReservePopulationSlots": () => false, // Always have pop space. + "UnReservePopulationSlots": () => {}, // Always have pop space. + "UnBlockTraining": () => {}, + "GetDisabledTemplates": () => ({}) +}); +const spyCmpPlayerSubtract = new Spy(cmpPlayer, "TrySubtractResources"); +const spyCmpPlayerRefund = new Spy(cmpPlayer, "RefundResources"); +const spyCmpPlayerPop = new Spy(cmpPlayer, "TryReservePopulationSlots"); + +ConstructComponent(playerEntityID, "EntityLimits", { + "Limits": { + "some_limit": 0 + }, + "LimitChangers": {}, + "LimitRemovers": {} +}); +// Test that we can't exceed the entity limit. +TS_ASSERT_EQUALS(cmpTrainer.QueueBatch(queuedUnit, 1), -1); +// And that in that case, the resources are not lost. +// ToDo: This is a bad test, it relies on the order of subtraction in the cmp. +// Better would it be to check the states before and after the queue. +TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, spyCmpPlayerRefund._called); + +ConstructComponent(playerEntityID, "EntityLimits", { + "Limits": { + "some_limit": 5 + }, + "LimitChangers": {}, + "LimitRemovers": {} +}); +let id = cmpTrainer.QueueBatch(queuedUnit, 1); +TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, 2); +TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1); + + +// Test removing a queued batch. +cmpTrainer.StopBatch(id); +TS_ASSERT_EQUALS(spyCmpPlayerRefund._called, 2); +TS_ASSERT_EQUALS(cmpTrainer.queue.size, 0); + +const cmpEntLimits = QueryOwnerInterface(entityID, IID_EntityLimits); +TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5)); + + +// Test finishing a queued batch. +id = cmpTrainer.QueueBatch(queuedUnit, 1); +TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4)); +TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0); +TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 500), 500); +TS_ASSERT_EQUALS(spyCmpPlayerPop._called, 1); +TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0.5); + +const spawedEntityIDs = [4, 5, 6, 7, 8]; +let spawned = 0; + +Engine.AddEntity = () => { + const ent = spawedEntityIDs[spawned++]; + + ConstructComponent(ent, "TrainingRestrictions", { + "Category": "some_limit" + }); + + AddMock(ent, IID_Identity, { + "GetClassesList": () => [] + }); + + AddMock(ent, IID_Position, { + "JumpTo": () => {} + }); + + AddMock(ent, IID_Ownership, { + "SetOwner": (pid) => { + QueryOwnerInterface(ent, IID_EntityLimits).OnGlobalOwnershipChanged({ + "entity": ent, + "from": -1, + "to": pid + }); + }, + "GetOwner": () => playerID + }); + + return ent; +}; +AddMock(entityID, IID_Footprint, { + "PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 }) +}); + +TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 1000), 500); +TS_ASSERT(!cmpTrainer.HasBatch(id)); +TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 5)); +TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4)); + +TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1); +TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1); + + +// Now check that it doesn't get updated when the spawn doesn't succeed. (regression_test_d1879) +cmpPlayer.TrySubtractResources = () => true; +cmpPlayer.RefundResources = () => {}; +AddMock(entityID, IID_Footprint, { + "PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 }) +}); +id = cmpTrainer.QueueBatch(queuedUnit, 2); +TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 2000), 2000); +TS_ASSERT(cmpTrainer.HasBatch(id)); + +TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); +TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 3); + +// Check that when the batch is removed the counts are subtracted again. +cmpTrainer.StopBatch(id); +TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1); +TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1); + +const queuedSecondUnit = "units/iber/cavalry_javelineer_b"; +// Check changing the allowed entities has effect. +const id1 = cmpTrainer.QueueBatch(queuedUnit, 1); +const id2 = cmpTrainer.QueueBatch(queuedSecondUnit, 1); +TS_ASSERT_EQUALS(cmpTrainer.queue.size, 2); +TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1).unitTemplate, queuedUnit); +TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, queuedSecondUnit); + +// Add a modifier that replaces unit A with unit C, +// adds a unit D and removes unit B from the roster. +Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => { + return HandleTokens(val, "units/{civ}/cavalry_javelineer_b>units/{civ}/c units/{civ}/d -units/{civ}/infantry_swordsman_b"); +}); + +cmpTrainer.OnValueModification({ + "component": "Trainer", + "valueNames": ["Trainer/Entities/_string"], + "entities": [entityID] +}); + +TS_ASSERT_UNEVAL_EQUALS( + cmpTrainer.GetEntitiesList(), ["units/iber/c", "units/iber/support_female_citizen", "units/iber/d"] +); +TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1); +TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1), undefined); +TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, "units/iber/c"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Trainer.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/plain \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/gaul_player_teambonus.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/gaul_player_teambonus.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/teambonuses/gaul_player_teambonus.json (revision 26000) @@ -1,14 +1,14 @@ { "type": "global", "affects": ["Forge"], "affectedPlayers": ["MutualAlly"], "modifications": [ - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.85 }, - { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.85 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.85 }, + { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.85 } ], "auraName": "Products from Gaul", "auraDescription": "Forges −15% technology resource costs and research time." } Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/athen_catafalque_2.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/athen_catafalque_2.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/athen_catafalque_2.json (revision 26000) @@ -1,12 +1,12 @@ { "type": "global", "affects": ["Economic"], "modifications": [ - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 } ], "auraName": "Economic Fortune", "auraDescription": "Solon brought in a new system of weights and measures, fathers were encouraged to find trades for their sons.\nEconomic technologies −10% resource costs." } Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/sele_catafalque_1.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/sele_catafalque_1.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/catafalques/sele_catafalque_1.json (revision 26000) @@ -1,16 +1,16 @@ { "type": "global", "affects": ["Temple"], "modifications": [ { "value": "Cost/Resources/food", "multiply": 0.9 }, { "value": "Cost/Resources/wood", "multiply": 0.9 }, { "value": "Cost/Resources/stone", "multiply": 0.9 }, { "value": "Cost/Resources/metal", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 } ], "auraName": "Founder of the Ezida Temple", "auraDescription": "Antiochus I laid the foundation for the Ezida Temple in Borsippa.\nTemples −10% resource costs; Temple technologies −10% resource costs." } Index: ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_ashoka.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_ashoka.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/auras/units/heroes/maur_hero_ashoka.json (revision 26000) @@ -1,17 +1,17 @@ { "type": "global", "affects": ["Temple"], "modifications": [ { "value": "Cost/BuildTime", "multiply": 0.5 }, { "value": "Cost/Resources/wood", "multiply": 0.5 }, { "value": "Cost/Resources/stone", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.5 }, - { "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 } + { "value": "Researcher/TechCostMultiplier/food", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.5 }, + { "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 } ], "auraDescription": "Temples −50% resource costs and build time. Temple technologies −50% resource costs and research time.", "auraName": "Buddhism", "overlayIcon": "art/textures/ui/session/auras/build_bonus.png" } Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/hoplite_tradition.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/technologies/hoplite_tradition.json (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/hoplite_tradition.json (revision 26000) @@ -1,29 +1,29 @@ -{ - "genericName": "Hoplite Tradition", - "description": "Hoplite soldiers constituted most of the armies of Greece.", - "cost": { - "food": 400, - "metal": 300 - }, - "requirements": { - "all": [ - { "tech": "phase_town" }, - { - "any": [ - { "civ": "athen" }, - { "civ": "spart" } - ] - } - ] - }, - "requirementsTooltip": "Unlocked in Town Phase.", - "icon": "armor_corinthian.png", - "researchTime": 60, - "tooltip": "Hoplites −25% training time and −50% promotion experience.", - "modifications": [ - { "value": "Cost/BuildTime", "multiply": 0.75 }, - { "value": "Promotion/RequiredXp", "multiply": 0.5 } - ], - "affects": ["Infantry Spearman !Hero"], - "soundComplete": "interface/alarm/alarm_upgradearmory.xml" -} +{ + "genericName": "Hoplite Tradition", + "description": "Hoplite soldiers constituted most of the armies of Greece.", + "cost": { + "food": 400, + "metal": 300 + }, + "requirements": { + "all": [ + { "tech": "phase_town" }, + { + "any": [ + { "civ": "athen" }, + { "civ": "spart" } + ] + } + ] + }, + "requirementsTooltip": "Unlocked in Town Phase.", + "icon": "armor_corinthian.png", + "researchTime": 60, + "tooltip": "Hoplites −25% training time and −50% promotion experience.", + "modifications": [ + { "value": "Cost/BuildTime", "multiply": 0.75 }, + { "value": "Promotion/RequiredXp", "multiply": 0.5 } + ], + "affects": ["Infantry Spearman !Hero"], + "soundComplete": "interface/alarm/alarm_upgradearmory.xml" +} Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 26000) @@ -1,1864 +1,1857 @@ // Setting this to true will display some warnings when commands // are likely to fail, which may be useful for debugging AIs var g_DebugCommands = false; function ProcessCommand(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; let data = { "cmpPlayer": cmpPlayer, "controlAllUnits": cmpPlayer.CanControlAllUnits() }; if (cmd.entities) data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits); // TODO: queuing order and forcing formations doesn't really work. // To play nice, we'll still no-formation queued order if units are in formation // but the opposite perhaps ought to be implemented. if (!cmd.queued || cmd.formation == NULL_FORMATION) data.formation = cmd.formation || undefined; // Allow focusing the camera on recent commands let commandData = { "type": "playercommand", "players": [player], "cmd": cmd }; // Save the position, since the GUI event is received after the unit died if (cmd.type == "delete-entities") { let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position); commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D(); } let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification(commandData); // Note: checks of UnitAI targets are not robust enough here, as ownership // can change after the order is issued, they should be checked by UnitAI // when the specific behavior (e.g. attack, garrison) is performed. // (Also it's not ideal if a command silently fails, it's nicer if UnitAI // moves the entities closer to the target before giving up.) // Now handle various commands if (g_Commands[cmd.type]) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("OnPlayerCommand", { "player": player, "cmd": cmd }); g_Commands[cmd.type](player, cmd, data); } else error("Invalid command: unknown command type: "+uneval(cmd)); } var g_Commands = { "aichat": function(player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); var notification = { "players": [player] }; for (var key in cmd) notification[key] = cmd[key]; cmpGuiInterface.PushNotification(notification); }, "cheat": function(player, cmd, data) { Cheat(cmd); }, "collect-treasure": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasure(cmd.target, cmd.queued); }); }, "collect-treasure-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.queued); }); }, "diplomacy": function(player, cmd, data) { let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (data.cmpPlayer.GetLockTeams() || cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive()) return; switch(cmd.to) { case "ally": data.cmpPlayer.SetAlly(cmd.player); break; case "neutral": data.cmpPlayer.SetNeutral(cmd.player); break; case "enemy": data.cmpPlayer.SetEnemy(cmd.player); break; default: warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "diplomacy", "players": [player], "targetPlayer": cmd.player, "status": cmd.to }); }, "tribute": function(player, cmd, data) { data.cmpPlayer.TributeResource(cmd.player, cmd.amounts); }, "control-all": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - control all units)") }); data.cmpPlayer.SetControlAllUnits(cmd.flag); }, "reveal-map": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - reveal map)") }); // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, cmd.enable); }, "walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront); }); }, "walk-custom": function(player, cmd, data) { for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront); }); }, "walk-to-range": function(player, cmd, data) { // Only used by the AI for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront); } }, "attack-walk": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront); }); }, "attack-walk-custom": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront); }); }, "attack": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; if (g_DebugCommands && !allowCapture && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued, cmd.pushFront); }); }, "patrol": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued) ); }, "heal": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront); }); }, "repair": function(player, cmd, data) { // This covers both repairing damaged buildings, and constructing unfinished foundations if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront); }); }, "gather": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront); }); }, "gather-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront); }); }, "returnresource": function(player, cmd, data) { if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront); }); }, "back-to-work": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(!cmpUnitAI || !cmpUnitAI.BackToWork()) notifyBackToWorkFailure(player); } }, "call-to-arms": function(player, cmd, data) { const unitsToMove = data.entities.filter(ent => MatchesClassList(Engine.QueryInterface(ent, IID_Identity).GetClassesList(), ["Soldier", "Warship", "Siege", "Healer"]) ); GetFormationUnitAIs(unitsToMove, player, cmd, data.formation).forEach(cmpUnitAI => { const target = cmd.target; if (cmd.pushFront) { cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, false, cmd.pushFront); cmpUnitAI.DropAtNearestDropSite(false, cmd.pushFront); } else { cmpUnitAI.DropAtNearestDropSite(cmd.queued, false) cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, true, false); } }); }, "remove-guard": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.RemoveGuard(); } }, "train": function(player, cmd, data) { if (!Number.isInteger(cmd.count) || cmd.count <= 0) { warn("Invalid command: can't train " + uneval(cmd.count) + " units"); return; } // Check entity limits var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var unitCategory = null; if (template.TrainingRestrictions) unitCategory = template.TrainingRestrictions.Category; // Verify that the building(s) can be controlled by the player if (data.entities.length <= 0) { if (g_DebugCommands) warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd)); return; } for (let ent of data.entities) { if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd)); continue; } } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: training requires unresearched technology: " + uneval(cmd)); continue; } - var queue = Engine.QueryInterface(ent, IID_ProductionQueue); + const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer); + if (!cmpTrainer) + continue; + + let templateName = cmd.template; // Check if the building can train the unit // TODO: the AI API does not take promotion technologies into account for the list // of trainable units (taken directly from the unit template). Here is a temporary fix. - if (queue && data.cmpPlayer.IsAI()) - { - var list = queue.GetEntitiesList(); - if (list.indexOf(cmd.template) === -1 && cmd.promoted) - { - for (var promoted of cmd.promoted) - { - if (list.indexOf(promoted) === -1) - continue; - cmd.template = promoted; - break; - } - } - } - if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1) - queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata, cmd.pushFront); + if (data.cmpPlayer.IsAI()) + templateName = cmpTrainer.GetUpgradedTemplate(cmd.template); + + if (cmpTrainer.CanTrain(templateName)) + Engine.QueryInterface(ent, IID_ProductionQueue)?.AddItem(templateName, "unit", +cmd.count, cmd.metadata, cmd.pushFront); } }, "research": function(player, cmd, data) { var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template)) { if (g_DebugCommands) warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd)); return; } var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.AddItem(cmd.template, "technology", undefined, cmd.metadata, cmd.pushFront); }, "stop-production": function(player, cmd, data) { let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.RemoveItem(cmd.id); }, "construct": function(player, cmd, data) { TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "construct-wall": function(player, cmd, data) { TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "delete-entities": function(player, cmd, data) { for (let ent of data.entities) { if (!data.controlAllUnits) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity && cmpIdentity.IsUndeletable()) continue; let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable && cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2) continue; let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather()) continue; } let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) { let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health); if (cmpMiragedHealth) cmpMiragedHealth.Kill(); else Engine.DestroyEntity(cmpMirage.parent); Engine.DestroyEntity(ent); continue; } let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } }, "set-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { if (!cmd.queued) cmpRallyPoint.Unset(); cmpRallyPoint.AddPosition(cmd.x, cmd.z); cmpRallyPoint.AddData(clone(cmd.data)); } } }, "unset-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Reset(); } }, "resign": function(player, cmd, data) { data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned.")); }, "occupy-turret": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.OccupyTurret(cmd.target, cmd.queued); }); }, "garrison": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront); }); }, "guard": function(player, cmd, data) { if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront); }); }, "stop": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Stop(cmd.queued); }); }, "leave-turret": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } if (notUnloaded) notifyUnloadFailure(player); }, "unload-turrets": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); for (let turret of cmpTurretHolder.GetEntities()) { let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } } if (notUnloaded) notifyUnloadFailure(player); }, "unload": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); var notUngarrisoned = 0; // The owner can ungarrison every garrisoned unit if (IsOwnedByPlayer(player, cmd.garrisonHolder)) data.entities = cmd.entities; for (let ent of data.entities) if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent)) ++notUngarrisoned; if (notUngarrisoned != 0) notifyUnloadFailure(player, cmd.garrisonHolder); }, "unload-template": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Only the owner of the garrisonHolder may unload entities from any owners if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits && player != +cmd.owner) continue; if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all)) notifyUnloadFailure(player, garrisonHolder); } } }, "unload-all-by-owner": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player)) notifyUnloadFailure(player, garrisonHolder); } }, "unload-all": function(player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) notifyUnloadFailure(player, garrisonHolder); } }, "alert-raise": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.RaiseAlert(); } }, "alert-end": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.EndOfAlert(); } }, "formation": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => { cmpUnitAI.MoveIntoFormation(cmd); }); }, "promote": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - promoted units)"), "translateMessage": true }); for (let ent of cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } }, "stance": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && !cmpUnitAI.IsTurret()) cmpUnitAI.SwitchToStance(cmd.name); } }, "lock-gate": function(player, cmd, data) { for (let ent of data.entities) { var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (!cmpGate) continue; if (cmd.lock) cmpGate.LockGate(); else cmpGate.UnlockGate(); } }, "setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); }, "cancel-setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CancelSetupTradeRoute(cmd.target); }); }, "set-trading-goods": function(player, cmd, data) { data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); }, "barter": function(player, cmd, data) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount); }, "set-shading-color": function(player, cmd, data) { // Prevent multiplayer abuse if (!data.cmpPlayer.IsAI()) return; // Debug command to make an entity brightly colored for (let ent of cmd.entities) { var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0 } }, "pack": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.Pack(cmd.queued, cmd.pushFront); else cmpUnitAI.Unpack(cmd.queued, cmd.pushFront); } }, "cancel-pack": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront); else cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront); } }, "upgrade": function(player, cmd, data) { for (let ent of data.entities) { var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template)) continue; if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template)) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.") }); continue; } // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template)) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd)); continue; } let cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); let requiredTechnology = cmpUpgrade.GetRequiredTechnology(cmd.template); if (requiredTechnology && (!cmpTechnologyManager || !cmpTechnologyManager.IsTechnologyResearched(requiredTechnology))) { if (g_DebugCommands) warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd)); continue; } cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer); } }, "cancel-upgrade": function(player, cmd, data) { for (let ent of data.entities) { let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) cmpUpgrade.CancelUpgrade(player); } }, "attack-request": function(player, cmd, data) { // Send a chat message to human players var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."), "translateParameters": ["_player_"], "parameters": { "_player_": cmd.player } }); // And send an attackRequest event to the AIs let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("AttackRequest", cmd); }, "spy-request": function(player, cmd, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => { let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing); return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player); })); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "spy-response", "players": [player], "target": cmd.player, "entity": ent }); if (ent) Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source); else { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy"); IncurBribeCost(template, player, cmd.player, true); // update statistics for failed bribes let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpBribesStatisticsTracker) cmpBribesStatisticsTracker.IncreaseFailedBribesCounter(); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("There are no bribable units"), "translateMessage": true }); } }, "diplomacy-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("DiplomacyRequest", cmd); }, "tribute-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("TributeRequest", cmd); }, "dialog-answer": function(player, cmd, data) { // Currently nothing. Triggers can read it anyway, and send this // message to any component you like. }, "set-dropsite-sharing": function(player, cmd, data) { for (let ent of data.entities) { let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite && cmpResourceDropsite.IsSharable()) cmpResourceDropsite.SetSharing(cmd.shared); } }, "map-flare": function(player, cmd, data) { let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "map-flare", "players": [player], "target": cmd.target }); }, "autoqueue-on": function(player, cmd, data) { for (let ent of data.entities) { let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.EnableAutoQueue(); } }, "autoqueue-off": function(player, cmd, data) { for (let ent of data.entities) { let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.DisableAutoQueue(); } }, }; /** * Sends a GUI notification about unit(s) that failed to ungarrison. */ function notifyUnloadFailure(player) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Unable to unload unit(s)."), "translateMessage": true }); } /** * Sends a GUI notification about worker(s) that failed to go back to work. */ function notifyBackToWorkFailure(player) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Some unit(s) can't go back to work"), "translateMessage": true }); } /** * Sends a GUI notification about entities that can't be controlled. * @param {number} player - The player-ID of the player that needs to receive this message. */ function notifyOrderFailure(entity, player) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); if (!cmpIdentity) return; let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("%(unit)s can't be controlled."), "parameters": { "unit": cmpIdentity.GetGenericName() }, "translateParameters": ["unit"], "translateMessage": true }); } /** * Get some information about the formations used by entities. */ function ExtractFormations(ents) { let entities = []; // Entities with UnitAI. let members = {}; // { formationentity: [ent, ent, ...], ... } let templates = {}; // { formationentity: template } for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; entities.push(ent); let fid = cmpUnitAI.GetFormationController(); if (fid == INVALID_ENTITY) continue; if (!members[fid]) { members[fid] = []; templates[fid] = cmpUnitAI.GetFormationTemplate(); } members[fid].push(ent); } return { "entities": entities, "members": members, "templates": templates }; } /** * Tries to find the best angle to put a dock at a given position * Taken from GuiInterface.js */ function GetDockAngle(template, x, z) { var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) return undefined; // Get footprint size var halfSize = 0; if (template.Footprint.Square) halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; else if (template.Footprint.Circle) halfSize = template.Footprint.Circle["@radius"]; /* Find direction of most open water, algorithm: * 1. Pick points in a circle around dock * 2. If point is in water, add to array * 3. Scan array looking for consecutive points * 4. Find longest sequence of consecutive points * 5. If sequence equals all points, no direction can be determined, * expand search outward and try (1) again * 6. Calculate angle using average of sequence */ const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { var angle = (i/numPoints)*2*Math.PI; var d = halfSize*(dist+1); var nx = x - d*Math.sin(angle); var nz = z + d*Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) waterPoints.push(i); } var consec = []; var length = waterPoints.length; if (!length) continue; for (var i = 0; i < length; ++i) { var count = 0; for (let j = 0; j < length - 1; ++j) { if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } var start = 0; var count = 0; for (var c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI; } return undefined; } /** * Attempts to construct a building using the specified parameters. * Returns true on success, false on failure. */ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) { // Message structure: // { // "type": "construct", // "entities": [...], // entities that will be ordered to construct the building (if applicable) // "template": "...", // template name of the entity being constructed // "x": ..., // "z": ..., // "angle": ..., // "metadata": "...", // AI metadata of the building // "actorSeed": ..., // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable) // "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. If specified, must be a valid control group ID (> 0). // "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. May be INVALID_ENTITY. // } /* * Construction process: * . Take resources away immediately. * . Create a foundation entity with 1hp, 0% build progress. * . Increase hp and build progress up to 100% when people work on it. * . If it's destroyed, an appropriate fraction of the resource cost is refunded. * . If it's completed, it gets replaced with the real building. */ // Check whether we can control these units var entities = FilterEntityList(cmd.entities, player, controlAllUnits); if (!entities.length) return false; var foundationTemplate = "foundation|" + cmd.template; // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity(foundationTemplate); if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) error("Error creating foundation entity for '" + cmd.template + "'"); return false; } // If it's a dock, get the right angle. var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var angle = cmd.angle; if (template.BuildRestrictions.PlacementType === "shore") { let angleDock = GetDockAngle(template, cmd.x, cmd.z); if (angleDock !== undefined) angle = angleDock; } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(angle); // Set the obstruction control group if needed if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2) { var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); // primary control group must always be valid if (cmd.obstructionControlGroup) { if (cmd.obstructionControlGroup <= 0) warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0"); cmpObstruction.SetControlGroup(cmd.obstructionControlGroup); } if (cmd.obstructionControlGroup2) cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2); } // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (cmpBuildRestrictions) { var ret = cmpBuildRestrictions.CheckPlacement(); if (!ret.success) { if (g_DebugCommands) warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); ret.players = [player]; cmpGuiInterface.PushNotification(ret); // Remove the foundation because the construction was aborted // move it out of world because it's not destroyed immediately. cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } } else error("cmpBuildRestrictions not defined"); // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("The building's technology requirements are not met."), "translateMessage": true }); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); } // We need the cost after tech and aura modifications. let cmpCost = Engine.QueryInterface(ent, IID_Cost); let costs = cmpCost.GetResourceCosts(); if (!cmpPlayer.TrySubtractResources(costs)) { if (g_DebugCommands) warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); Engine.DestroyEntity(ent); cmpPosition.MoveOutOfWorld(); return false; } var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual && cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); cmpFoundation.InitialiseConstruction(cmd.template); // send Metadata info if any if (cmd.metadata) Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } ); // Tell the units to start building this new entity if (cmd.autorepair) { ProcessCommand(player, { "type": "repair", "entities": entities, "target": ent, "autocontinue": cmd.autocontinue, "queued": cmd.queued, "pushFront": cmd.pushFront, "formation": cmd.formation || undefined }); } return ent; } function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd) { // 'cmd' message structure: // { // "type": "construct-wall", // "entities": [...], // entities that will be ordered to construct the wall (if applicable) // "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...) // { // "template": "...", // one of the templates from the wallset // "x": ..., // "z": ..., // "angle": ..., // }, // ... // ], // "wallSet": { // "templates": { // "tower": // tower template name // "long": // long wall segment template name // ... // etc. // }, // "maxTowerOverlap": ..., // "minTowerOverlap": ..., // }, // "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall // "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable) // } if (cmd.pieces.length <= 0) return; if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side"); return; } if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side"); return; } // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing // towers in the case of snapping). The towers themselves all keep their default unique control groups. // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of // the first tower encountered towards the ending side of the wall (if any). // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes: // // FIRST PASS: // - Go from start to end and construct wall piece foundations as far as we can without running into a piece that // cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID // as the primary control group, thus allowing it to be built overlapping the previous piece. // - If we encounter a new tower along the way (which will gain its own control group), do the following: // o First build it using temporarily the same control group of the previous (non-tower) piece // o Set the previous piece's secondary control group to the tower's entity ID // o Restore the primary control group of the constructed tower back its original (unique) value. // The temporary control group is necessary to allow the newer tower with its unique control group ID to be able // to be placed while overlapping the previous piece. // // SECOND PASS: // - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this // time registering the right neighbouring tower in each non-tower piece. // first pass; L -> R var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that // the first wall piece can be built while overlapping it. if (cmd.startSnappedEntity) { var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction); if (!cmpSnappedStartObstruction) { error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup(); //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup); } var i = 0; var queued = cmd.queued; var pieces = clone(cmd.pieces); for (; i < pieces.length; ++i) { var piece = pieces[i]; // All wall pieces after the first must be queued. if (i > 0 && !queued) queued = true; // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do // start position snapping (implying that the first entity we build must be a tower) if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY) { if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity)) { error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")"); break; } } var constructPieceCmd = { "type": "construct", "entities": cmd.entities, "template": piece.template, "x": piece.x, "z": piece.z, "angle": piece.angle, "autorepair": cmd.autorepair, "autocontinue": cmd.autocontinue, "queued": queued, // Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed // using the control group of the last tower (see comments above). "obstructionControlGroup": lastTowerControlGroup, }; // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's // control group directly at construction time (instead of setting it in the second pass) to allow it to be built // while overlapping the snapped entity. if (i == pieces.length - 1 && cmd.endSnappedEntity) { var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (cmpEndSnappedObstruction) constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup(); } var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd); if (pieceEntityId) { // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later piece.ent = pieceEntityId; // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex if (piece.template == cmd.wallSet.templates.tower) { var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction); var newTowerControlGroup = pieceEntityId; if (i > 0) { //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup); var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction); // TODO: ensure that cmpPreviousObstruction exists // TODO: ensure that the previous obstruction does not yet have a secondary control group set cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); } // TODO: ensure that cmpTowerObstruction exists cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group lastTowerIndex = i; lastTowerControlGroup = newTowerControlGroup; } } else // failed to build wall piece, abort break; } var lastBuiltPieceIndex = i - 1; var wallComplete = (lastBuiltPieceIndex == pieces.length - 1); // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower). // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any) // as their secondary control groups. lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // only start off with the ending side's snapped tower's control group if we were able to build the entire wall if (cmd.endSnappedEntity && wallComplete) { var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (!cmpSnappedEndObstruction) { error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup(); } for (var j = lastBuiltPieceIndex; j >= 0; --j) { var piece = pieces[j]; if (!piece.ent) { error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'"); continue; } var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction); if (!cmpPieceObstruction) { error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component"); continue; } if (piece.template == cmd.wallSet.templates.tower) { // encountered a tower entity, update the last tower control group lastTowerControlGroup = cmpPieceObstruction.GetControlGroup(); } else { // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'. // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'. var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2(); if (existingSecondaryControlGroup == INVALID_ENTITY) { if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY) { cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup); } } else if (existingSecondaryControlGroup != lastTowerControlGroup) { error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); break; } } } } /** * Remove the given list of entities from their current formations. */ function RemoveFromFormation(ents) { let formation = ExtractFormations(ents); for (let fid in formation.members) { let cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } } /** * Returns a list of UnitAI components, each belonging either to a * selected unit or to a formation entity for groups of the selected units. */ function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate) { // If an individual was selected, remove it from any formation // and command it individually. if (ents.length == 1) { let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) return []; RemoveFromFormation(ents); return [ cmpUnitAI ]; } let formationUnitAIs = []; // Find what formations the selected entities are currently in, // and default to that unless the formation is forced or it's the null formation // (we want that to reset whatever formations units are in). if (formationTemplate != NULL_FORMATION) { let formation = ExtractFormations(ents); let formationIds = Object.keys(formation.members); if (formationIds.length == 1) { // Selected units either belong to this formation or have no formation. let fid = formationIds[0]; let cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length && cmpFormation.GetMemberCount() == formation.entities.length) { cmpFormation.DeleteTwinFormations(); // The whole formation was selected, so reuse its controller for this command. if (!forceTemplate || formationTemplate == formation.templates[fid]) { formationTemplate = formation.templates[fid]; formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; } else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)]; } else if (cmpFormation && !forceTemplate) { // Just reuse the template. formationTemplate = formation.templates[fid]; } } else if (formationIds.length) { // Check if all entities share a common formation, if so reuse this template. let template = formation.templates[formationIds[0]]; for (let i = 1; i < formationIds.length; ++i) if (formation.templates[formationIds[i]] != template) { template = null; break; } if (template && !forceTemplate) formationTemplate = template; } } // Separate out the units that don't support the chosen formation. let formedUnits = []; let nonformedUnitAIs = []; for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) continue; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: We only check if the formation is usable by some units // if we move them to it. We should check if we can use formations // for the other cases. let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION; if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION)) { if (nullFormation && cmpUnitAI.GetFormationController()) cmpUnitAI.LeaveFormation(cmd.queued || false); nonformedUnitAIs.push(cmpUnitAI); } else formedUnits.push(ent); } if (nonformedUnitAIs.length == ents.length) { // No units support the formation. return nonformedUnitAIs; } if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller. // TODO replace the fixed 60 with something sensible, based on vision range f.e. let formationSeparation = 60; let clusters = ClusterEntities(formedUnits, formationSeparation); let formationEnts = []; for (let cluster of clusters) { RemoveFromFormation(cluster); if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) { for (let ent of cluster) nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI)); continue; } // Create the new controller. let formationEnt = Engine.AddEntity(formationTemplate); let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); cmpFormation.SetFormationSeparation(formationSeparation); cmpFormation.SetMembers(cluster); for (let ent of formationEnts) cmpFormation.RegisterTwinFormation(ent); formationEnts.push(formationEnt); let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); cmpOwnership.SetOwner(player); } } return nonformedUnitAIs.concat(formationUnitAIs); } /** * Group a list of entities in clusters via single-links */ function ClusterEntities(ents, separationDistance) { let clusters = []; if (!ents.length) return clusters; let distSq = separationDistance * separationDistance; let positions = []; // triangular matrix with the (squared) distances between the different clusters // the other half is not initialised let matrix = []; for (let i = 0; i < ents.length; ++i) { matrix[i] = []; clusters.push([ents[i]]); let cmpPosition = Engine.QueryInterface(ents[i], IID_Position); positions.push(cmpPosition.GetPosition2D()); for (let j = 0; j < i; ++j) matrix[i][j] = positions[i].distanceToSquared(positions[j]); } while (clusters.length > 1) { // search two clusters that are closer than the required distance let closeClusters = undefined; for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i) for (let j = i - 1; j >= 0 && !closeClusters; --j) if (matrix[i][j] < distSq) closeClusters = [i,j]; // if no more close clusters found, just return all found clusters so far if (!closeClusters) return clusters; // make a new cluster with the entities from the two found clusters let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); // calculate the minimum distance between the new cluster and all other remaining // clusters by taking the minimum of the two distances. let distances = []; for (let i = 0; i < clusters.length; ++i) { let a = closeClusters[1]; let b = closeClusters[0]; if (i == a || i == b) continue; let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a]; let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b]; distances.push(Math.min(dist1, dist2)); } // remove the rows and columns in the matrix for the merged clusters, // and the clusters themselves from the cluster list clusters.splice(closeClusters[0],1); clusters.splice(closeClusters[1],1); matrix.splice(closeClusters[0],1); matrix.splice(closeClusters[1],1); for (let i = 0; i < matrix.length; ++i) { if (matrix[i].length > closeClusters[0]) matrix[i].splice(closeClusters[0],1); if (matrix[i].length > closeClusters[1]) matrix[i].splice(closeClusters[1],1); } // add a new row of distances to the matrix and the new cluster clusters.push(newCluster); matrix.push(distances); } return clusters; } function GetFormationRequirements(formationTemplate) { var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate); if (!template.Formation) return false; return { "minCount": +template.Formation.RequiredMemberCount }; } function CanMoveEntsIntoFormation(ents, formationTemplate) { // TODO: should check the player's civ is allowed to use this formation // See simulation/components/Player.js GetFormations() for a list of all allowed formations var requirements = GetFormationRequirements(formationTemplate); if (!requirements) return false; var count = 0; for (let ent of ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate)) continue; ++count; } return count >= requirements.minCount; } /** * Check if player can control this entity * returns: true if the entity is owned by the player and controllable * or control all units is activated, else false */ function CanControlUnit(entity, player, controlAll) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); let canBeControlled = IsOwnedByPlayer(player, entity) && (!cmpIdentity || cmpIdentity.IsControllable()) || controlAll; if (!canBeControlled) notifyOrderFailure(entity, player); return canBeControlled; } /** * @param {number} entity - The entityID to verify. * @param {number} player - The playerID to check against. * @return {boolean}. */ function IsOwnedByPlayerOrMutualAlly(entity, player) { return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity); } /** * Check if player can control this entity * @return {boolean} - True if the entity is valid and controlled by the player * or the entity is owned by an mutualAlly and can be controlled * or control all units is activated, else false. */ function CanPlayerOrAllyControlUnit(entity, player, controlAll) { return CanControlUnit(player, entity, controlAll) || IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity); } /** * @return {boolean} - Whether the owner of this entity can control the entity. */ function CanOwnerControlEntity(entity) { let cmpOwner = QueryOwnerInterface(entity); return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID()); } /** * Filter entities which the player can control. */ function FilterEntityList(entities, player, controlAll) { return entities.filter(ent => CanControlUnit(ent, player, controlAll)); } /** * Filter entities which the player can control or are mutualAlly */ function FilterEntityListWithAllies(entities, player, controlAll) { return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll)); } /** * Incur the player with the cost of a bribe, optionally multiply the cost with * the additionalMultiplier */ function IncurBribeCost(template, player, playerBribed, failedBribe) { let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed); if (!cmpPlayerBribed) return false; let costs = {}; // Additional cost for this owner let multiplier = cmpPlayerBribed.GetSpyCostMultiplier(); if (failedBribe) multiplier *= template.VisionSharing.FailureCostRatio; for (let res in template.Cost.Resources) costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template)); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer.TrySubtractResources(costs); } Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("GetDockAngle", GetDockAngle); Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Engine.RegisterGlobal("g_Commands", g_Commands); Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost); Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_religious_test.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_religious_test.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_religious_test.xml (revision 26000) @@ -1,27 +1,29 @@ 12.0 2200 athen Religious Sanctuary Greek Religious Sanctuary + true 100 65535 + structures/hellenes/temple.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_arsenal.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_arsenal.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_arsenal.xml (revision 26000) @@ -1,13 +1,15 @@ skirm + structures/{civ}/arsenal + structures/hellenes/workshop.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_corral.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_corral.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_corral.xml (revision 26000) @@ -1,19 +1,21 @@ skirm + structures/{civ}/corral + structures/hellenes/corral.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_fortress.xml (revision 26000) @@ -1,13 +1,15 @@ skirm + structures/{civ}/fortress + structures/athenians/fortress.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_range.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_range.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_range.xml (revision 26000) @@ -1,19 +1,21 @@ skirm + structures/{civ}/range + structures/hellenes/range.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_market.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_market.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_market.xml (revision 26000) @@ -1,13 +1,15 @@ skirm + structures/{civ}/market + structures/hellenes/market.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_5.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_5.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_5.xml (revision 26000) @@ -1,18 +1,20 @@ skirm Changes in a 5-pop house for civilisations with those houses, is deleted for other civs + + structures/ptolemies/house.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_10.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_10.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_house_10.xml (revision 26000) @@ -1,12 +1,14 @@ skirm Changes in a 10-pop house for civilisations with those houses, is deleted for other civs + + structures/hellenes/house.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_stable.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_stable.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_stable.xml (revision 26000) @@ -1,19 +1,21 @@ skirm + structures/{civ}/stable + structures/hellenes/stable.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/civil_centre.xml (revision 26000) @@ -1,20 +1,20 @@ decay|rubble/rubble_hele_cc athen Agora - + units/{civ}/infantry_spearman_b units/{civ}/infantry_slinger_b units/{civ}/cavalry_javelineer_b - + structures/athenians/civil_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/bench.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/bench.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/bench.xml (revision 26000) @@ -1,28 +1,30 @@ 4.0 75 Bench Wooden Bench gaia/special_fence.png + 6.0 + props/special/eyecandy/bench_1.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/civil_centre.xml (revision 26000) @@ -1,23 +1,25 @@ decay|rubble/rubble_kart_cc cart Merkāz - + + + colonization + + + units/{civ}/infantry_spearman_b units/{civ}/infantry_archer_b units/{civ}/cavalry_javelineer_b - - colonization - - + structures/carthaginians/civil_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_iberian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_iberian.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_iberian.xml (revision 26000) @@ -1,31 +1,31 @@ 200 cart Iberian Embassy Train Iberian Mercenaries and research Mercenary technologies. CivSpecific structures/embassy_iberian.png 40 - + units/{native}/infantry_javelineer_iber_b units/{native}/infantry_slinger_iber_b units/{native}/cavalry_swordsman_iber_b - + structures/carthaginians/embassy_iberian.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/temple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/temple.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/temple.xml (revision 26000) @@ -1,23 +1,23 @@ 12.0 cart Maqdaš Train Healers and Champion Infantry and research healing technologies. - + units/{civ}/champion_infantry - + structures/carthaginians/temple_big.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen.xml (revision 26000) @@ -1,28 +1,30 @@ 4.0 220 Column Fallen Doric Column gaia/special_fence.png 20 + 6.0 + props/special/eyecandy/column_doric_fallen.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_stone.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_stone.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_stone.xml (revision 26000) @@ -1,30 +1,32 @@ 4.0 220 Stone Fence Stone Fence gaia/special_fence.png 20 + 6.0 + props/special/eyecandy/fence_stone_medit.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_epic_temple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_epic_temple.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_epic_temple.xml (revision 26000) @@ -1,46 +1,48 @@ 500 0 0 1000 500 12.0 30 5000 Epic Temple Naos Parthenos Garrison units to heal them at a quick rate. 200 500 + 60 65535 + 80 structures/fndn_6x6.xml structures/hellenes/temple_epic.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/fortress.xml (revision 26000) @@ -1,24 +1,24 @@ iber Castro Train Heroes. Garrison Soldiers for additional arrows. - + units/{civ}/hero_caros units/{civ}/hero_indibil units/{civ}/hero_viriato - + structures/iberians/fortress.xml 24.5 Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_wonder.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_wonder.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_wonder.xml (revision 26000) @@ -1,19 +1,21 @@ skirm + structures/{civ}/wonder + structures/hellenes/temple_epic.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/prytaneion.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/prytaneion.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/prytaneion.xml (revision 26000) @@ -1,65 +1,67 @@ Council 200 100 200 8.0 2000 athen Council Chamber Prytaneion Train Heroes and research technologies. ConquestCritical CivSpecific Council structures/tholos.png 20 40 - - 0.7 - - units/{civ}/hero_themistocles - units/{civ}/hero_pericles - units/{civ}/hero_iphicrates - + long_walls iphicratean_reforms - + interface/complete/building/complete_tholos.xml false 38 40000 + + 0.7 + + units/{civ}/hero_themistocles + units/{civ}/hero_pericles + units/{civ}/hero_iphicrates + + 40 structures/athenians/prytaneion.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/fortress.xml (revision 26000) @@ -1,38 +1,38 @@ 18.0 decay|rubble/rubble_stone_5x5 brit Dunon Train Heroes. Garrison Soldiers for additional arrows. - - - units/{civ}/hero_boudicca - units/{civ}/hero_caratacos - units/{civ}/hero_cunobelin - - interface/complete/building/complete_broch.xml + + + units/{civ}/hero_boudicca + units/{civ}/hero_caratacos + units/{civ}/hero_cunobelin + + structures/britons/fortress.xml structures/fndn_9x9.xml 28 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_celtic.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_celtic.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_celtic.xml (revision 26000) @@ -1,37 +1,37 @@ 200 11.0 1200 cart Celtic Embassy Train Celtic Mercenaries and research Mercenary technologies. CivSpecific structures/embassy_celtic.png 40 - + units/{native}/infantry_swordsman_gaul_b units/{native}/cavalry_swordsman_gaul_b - + structures/carthaginians/embassy_celtic.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/super_dock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/super_dock.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/super_dock.xml (revision 26000) @@ -1,94 +1,94 @@ structures/cart_super_dock_repair own ally neutral shore Dock 4 10 500 300 200 8.0 5 0.1 Unit Support Infantry Cavalry Ship 0 2 5000 cart Naval Shipyard Cothon Build upon a shoreline in own, neutral, or allied territory. Acquire large tracts of territory. Territory root. Construct Warships and research technologies. ConquestCritical CivSpecific Naval Shipyard structures/uber_dock.png 60 40 true 0.0 - - 0.7 - - units/{civ}/ship_bireme - units/{civ}/ship_trireme - units/{civ}/ship_quinquereme - - ship 35 interface/complete/building/complete_dock.xml true 200 25000 + + 0.7 + + units/{civ}/ship_bireme + units/{civ}/ship_trireme + units/{civ}/ship_quinquereme + + 100 structures/carthaginians/super_dock.xml structures/fndn_dock_super.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric.xml (revision 26000) @@ -1,28 +1,30 @@ 4.0 220 Column Doric Column gaia/special_fence.png 20 + 8.0 + props/special/eyecandy/column_doric.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_short.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_short.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_short.xml (revision 26000) @@ -1,37 +1,39 @@ 4.0 50 Fence Short Wooden Fence gaia/special_fence.png + 6.0 + true temp/fence_wood_short_a.xml 6 0.5 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/temple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/temple.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/temple.xml (revision 26000) @@ -1,36 +1,36 @@ 300 0 8.0 decay|rubble/rubble_stone_5x5 gaul Nemeton 60 0 - + units/{civ}/champion_fanatic - + structures/celts/temple.xml structures/fndn_7x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/civil_centre.xml (revision 26000) @@ -1,20 +1,20 @@ decay|rubble/rubble_iber_cc iber Oppidum - + units/{civ}/infantry_swordsman_b units/{civ}/infantry_javelineer_b units/{civ}/cavalry_javelineer_b - + structures/iberians/civic_center.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_blemmye.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_blemmye.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_blemmye.xml (revision 26000) @@ -1,47 +1,47 @@ own neutral 100 100 12.0 1000 decay|rubble/rubble_stone_5x5 kush Blemmye Camp Train Blemmye Mercenaries. CivSpecific structures/mercenary_camp.png 20 20 - - - units/{native}/cavalry_javelineer_merc_b - - 1 + + + units/{native}/cavalry_javelineer_merc_b + + structures/mercenaries/camp_blemmye.xml structures/fndn_8x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_temple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_temple.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/skirmish/structures/default_temple.xml (revision 26000) @@ -1,13 +1,15 @@ skirm + structures/{civ}/temple + structures/athenians/temple.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml (revision 26000) @@ -1,56 +1,56 @@ 200 200 200 8.0 10 2000 athen Gymnasium Gymnasion Train Champions. ConquestCritical CivSpecific Gymnasium structures/gymnasium.png 40 40 - - 0.7 - - units/{civ}/champion_infantry - units/{civ}/champion_ranged - - interface/complete/building/complete_gymnasium.xml + + 0.7 + + units/{civ}/champion_infantry + units/{civ}/champion_ranged + + 40 structures/athenians/gymnasium.xml structures/fndn_8x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml (revision 26000) @@ -1,50 +1,52 @@ own ally neutral shore 8.0 brit Island Settlement Cranogion Build upon a shoreline in own, neutral, or allied territory. Acquire large tracts of territory. Territory root. Train Citizens, construct Ships, and research technologies. Garrison Soldiers for additional arrows. CivSpecific Naval structures/crannog.png phase_town true 0.0 - + + ship + + + + -phase_town_{civ} + -hellenistic_metropolis + + + units/{civ}/infantry_spearman_b units/{civ}/infantry_slinger_b units/{civ}/cavalry_javelineer_b units/{civ}/ship_fishing units/{civ}/ship_merchant units/{civ}/ship_bireme units/{civ}/ship_trireme - - -phase_town_{civ} - -hellenistic_metropolis - - - - ship - + structures/britons/crannog.xml structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy.xml (revision 26000) @@ -1,44 +1,44 @@ 400 200 15.0 decay|rubble/rubble_stone_5x5 cart Embassy Train Mercenaries and research Mercenary technologies. structures/embassy_italic.png 80 40 - + units/{native}/infantry_swordsman_gaul_b units/{native}/cavalry_swordsman_gaul_b units/{native}/infantry_javelineer_iber_b units/{native}/infantry_slinger_iber_b units/{native}/cavalry_swordsman_iber_b units/{native}/infantry_swordsman_ital_b units/{native}/cavalry_spearman_ital_b - + structures/carthaginians/embassy.xml structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/fortress.xml (revision 26000) @@ -1,30 +1,30 @@ 8.0 cart Ḥamet Train Heroes. Garrison Soldiers for additional arrows. - + units/{civ}/hero_hamilcar units/{civ}/hero_hannibal units/{civ}/hero_maharbal - + structures/carthaginians/fndn_fortress.xml structures/carthaginians/fortress.xml 25.0 6.4 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_longhouse.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_longhouse.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_longhouse.xml (revision 26000) @@ -1,32 +1,34 @@ 300 0 100 100 0 10.0 1000 Longhouse 10 + + structures/celts/longhouse.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_long.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_long.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/fence_long.xml (revision 26000) @@ -1,37 +1,39 @@ 4.0 100 Fence Long Wooden Fence gaia/special_fence.png 20 + 6.0 + temp/fence_wood_long_a.xml 12 0.5 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/civil_centre.xml (revision 26000) @@ -1,27 +1,27 @@ 8.0 decay|rubble/rubble_gaul_cc gaul Lissos - + units/{civ}/infantry_spearman_b units/{civ}/infantry_javelineer_b units/{civ}/cavalry_javelineer_b - + structures/gauls/civic_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_stoa.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_stoa.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_stoa.xml (revision 26000) @@ -1,48 +1,50 @@ Stoa 110 0 0 100 100 10.0 1100 Stoa Hellenic Stoa gaia/special_stoa.png 50 50 + false 36 65535 + 40 special/greek_stoa.xml structures/fndn_6x4.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ishtar_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ishtar_gate.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ishtar_gate.xml (revision 26000) @@ -1,60 +1,62 @@ structures/loyalty_regen 200 250 500 8.0 10 2000 Ishtar Gate of Babylon Territory root. structures/pers_gate.png 50 100 + interface/complete/building/complete_broch.xml true 38 40000 + 40 special/pers_ishtar_gate.xml structures/fndn_9x4.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/dock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/dock.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/dock.xml (revision 26000) @@ -1,28 +1,28 @@ 8.0 decay|rubble/rubble_hele_dock athen Limēn - + 0.7 units/{civ}/infantry_marine_archer_b units/{civ}/champion_marine - + structures/athenians/dock.xml structures/fndn_6x4_dock.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/civil_centre.xml (revision 26000) @@ -1,28 +1,28 @@ 8.0 decay|rubble/rubble_brit_cc brit Tigernotreba - + units/{civ}/infantry_spearman_b units/{civ}/infantry_slinger_b units/{civ}/cavalry_javelineer_b - + structures/britons/civic_centre.xml structures/fndn_7x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/corral.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/corral.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/corral.xml (revision 26000) @@ -1,22 +1,22 @@ cart Rēfet - + -gaia/fauna_cattle_cow_trainable gaia/fauna_cattle_sanga_trainable - + structures/carthaginians/corral.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_italic.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_italic.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/embassy_italic.xml (revision 26000) @@ -1,37 +1,37 @@ 200 12.0 1500 cart Italic Embassy Train Italic Mercenaries and research Mercenary technologies. CivSpecific structures/embassy_italic.png 40 - + units/{native}/infantry_swordsman_ital_b units/{native}/cavalry_spearman_ital_b - + structures/carthaginians/embassy_italic.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_hut.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_hut.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/celt_hut.xml (revision 26000) @@ -1,35 +1,37 @@ 0 50 0 0 5.0 500 decay|rubble/rubble_stone_2x2 Hut 2 + 7.0 + structures/celts/hut.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen_b.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/column_doric_fallen_b.xml (revision 26000) @@ -1,28 +1,30 @@ 4.0 110 Column Fallen Doric Column gaia/special_fence.png 10 + 6.0 + props/special/eyecandy/column_doric_fallen_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml (revision 26000) @@ -1,77 +1,77 @@ 200 400 10.0 20 0.1 Unit Support Infantry Cavalry 0 2 2000 decay|rubble/rubble_stone_6x6 gaul Assembly of Princes Remogantion Train Champion Trumpeters and Heroes. ConquestCritical CivSpecific City Council structures/tholos.png phase_city 80 - - 0.7 - - units/{civ}/champion_infantry_trumpeter - units/{civ}/hero_brennus - units/{civ}/hero_viridomarus - units/{civ}/hero_vercingetorix - - 20 30 3 interface/complete/building/complete_iber_monument.xml false 40 40000 + + 0.7 + + units/{civ}/champion_infantry_trumpeter + units/{civ}/hero_brennus + units/{civ}/hero_viridomarus + units/{civ}/hero_vercingetorix + + 40 structures/gauls/theater.xml structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_propylaea.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_propylaea.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/hellenic_propylaea.xml (revision 26000) @@ -1,44 +1,46 @@ 200 0 0 200 200 8.0 2000 Portico Propylaea structures/tholos.png 75 75 + false 40 65535 + 20 structures/hellenes/propylaea.xml structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 26000) @@ -1,67 +1,69 @@ structures/iber_monument Monument Monument 150 120 100 100 8.0 1200 decay|rubble/rubble_stone_2x2 iber Revered Monument Gur Oroigarri CivSpecific Monument Town structures/iberian_bull.png phase_town 20 20 + 20 30 3 interface/complete/building/complete_iber_monument.xml + structures/iberians/sb_1.xml structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_noba.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_noba.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/camp_noba.xml (revision 26000) @@ -1,48 +1,48 @@ own neutral 100 100 12.0 1000 decay|rubble/rubble_stone_5x5 kush Noba Village Train Noba Mercenaries. CivSpecific structures/mercenary_camp.png 20 20 - - - units/{native}/infantry_maceman_merc_b - units/{native}/infantry_javelineer_merc_b - - 1 + + + units/{native}/infantry_maceman_merc_b + units/{native}/infantry_javelineer_merc_b + + structures/mercenaries/camp_nuba.xml structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml (revision 26000) @@ -1,62 +1,64 @@ structures/kush_pyramids_military PyramidLarge 300 450 150 20.0 3000 decay|rubble/rubble_stone_6x6 kush Large Pyramid mr ʿȝ -ConquestCritical CivSpecific City Pyramid phase_city structures/kush_pyramid_big.png 90 30 + interface/complete/building/complete_iber_monument.xml 15.0 false 40 40000 + 40 structures/kushites/pyramid_large.xml structures/fndn_5x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/fortress.xml (revision 26000) @@ -1,29 +1,29 @@ 8.0 kush Htm Train Heroes. Garrison Soldiers for additional arrows. - + units/{civ}/hero_nastasen units/{civ}/hero_amanirenas units/{civ}/hero_arakamani - + structures/kushites/fortress.xml 28.5 6.7 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/corral.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/corral.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/corral.xml (revision 26000) @@ -1,24 +1,24 @@ 8.0 kush ihy - + -gaia/fauna_cattle_cow_trainable gaia/fauna_cattle_sanga_trainable - + structures/kushites/corral.xml structures/fndn_4x4.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/civil_centre.xml (revision 26000) @@ -1,27 +1,29 @@ 10.0 kush Pr-nsw - + + + architecture_kush + + + units/{civ}/infantry_spearman_b units/{civ}/infantry_archer_b units/{civ}/cavalry_javelineer_b - - architecture_kush - - + structures/kushites/civic_centre_kush.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml (revision 26000) @@ -1,59 +1,61 @@ structures/kush_pyramids_economic PyramidSmall 200 300 100 15.0 2000 decay|rubble/rubble_stone_4x4 kush Small Pyramid mr -ConquestCritical CivSpecific Town Pyramid phase_town structures/kush_pyramid_small.png 60 20 + interface/complete/building/complete_iber_monument.xml false 30 30000 + 30 structures/kushites/pyramid_small.xml structures/fndn_4x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml (revision 26000) @@ -1,55 +1,55 @@ structures/kush_temple_amun TempleOfAmun 2 2 2 2 decay|rubble/rubble_stone_6x6 kush Grand Temple of Amun Pr-ʿImn Train Amun Champions and Elite Healers. Research healing technologies. CivSpecific -Town City TempleOfAmun structures/temple_epic.png phase_city 2 - + + 2 + + -units/{civ}/support_healer_b units/{civ}/support_healer_e units/{civ}/champion_infantry_amun - - - 2 - + structures/kushites/temple_amun.xml structures/fndn_9x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/corral.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/corral.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/corral.xml (revision 26000) @@ -1,22 +1,22 @@ maur Gotra - + -gaia/fauna_cattle_cow_trainable gaia/fauna_cattle_zebu_trainable - + structures/mauryas/corral.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/obelisk.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/obelisk.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/obelisk.xml (revision 26000) @@ -1,27 +1,29 @@ 10.0 1200 Obelisk Egyptian Obelisk gaia/special_obelisk.png 200 200 + + props/special/eyecandy/obelisk.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/tacara.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/tacara.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/tacara.xml (revision 26000) @@ -1,55 +1,57 @@ 300 200 400 8.0 10 3000 decay|rubble/rubble_stone_6x6 pers Palace Taçara ConquestCritical Palace structures/palace.png 40 80 + interface/complete/building/complete_broch.xml true 48 40000 + 40 structures/persians/palace.xml structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/lighthouse.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/lighthouse.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/lighthouse.xml (revision 26000) @@ -1,58 +1,60 @@ own ally neutral shore Lighthouse 200 200 200 8.0 2000 decay|rubble/rubble_stone_4x6 ptol Lighthouse Pharos Build upon a shoreline in own, neutral, or allied territory. Very large vision range. CivSpecific Lighthouse structures/lighthouse.png 40 40 true 0.0 + interface/complete/building/complete_temple.xml + 200 structures/ptolemies/lighthouse.xml structures/fndn_4x4_dock.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 26000) @@ -1,127 +1,127 @@ Bow 10 60 1200 2000 100 1.5 50 false Human outline_border.png outline_border_mask.png 0.175 - - 3 - 15 - 1 - Soldier - neutral enemy ArmyCamp ArmyCamp 80 + + 3 + 15 + 1 + Soldier + 3 10.0 3.0 250 500 100 100 12.0 20 0.1 Unit Support Infantry Cavalry Siege 0 6 2250 decay|rubble/rubble_rome_sb rome Army Camp Castra Build in neutral or enemy territory. Train Advanced Melee Infantry. Construct Rams. Garrison Soldiers for additional arrows. ConquestCritical CivSpecific City ArmyCamp structures/roman_camp.png phase_city 100 - - 0.7 - - units/{civ}/infantry_axeman_a - units/{civ}/infantry_swordsman_a - units/{civ}/infantry_spearman_a - units/{civ}/infantry_pikeman_a - units/{civ}/siege_ram - - 15 35 3 interface/complete/building/complete_broch.xml attack/weapon/bow_attack.xml attack/impact/arrow_impact.xml 2 + + 0.7 + + units/{civ}/infantry_axeman_a + units/{civ}/infantry_swordsman_a + units/{civ}/infantry_spearman_a + units/{civ}/infantry_pikeman_a + units/{civ}/siege_ram + + 90 structures/romans/camp.xml structures/fndn_8x8.xml 29.5 8 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/civil_centre.xml (revision 26000) @@ -1,20 +1,20 @@ sele Agora - + units/{civ}/infantry_spearman_b units/{civ}/infantry_javelineer_b units/{civ}/cavalry_javelineer_b units/{civ}/hero_seleucus_i units/{civ}/hero_antiochus_iii units/{civ}/hero_antiochus_iv - + structures/seleucids/civic_center.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/civil_centre.xml (revision 26000) @@ -1,20 +1,20 @@ decay|rubble/rubble_hele_cc spart Agora - + units/{civ}/infantry_spearman_b units/{civ}/infantry_javelineer_b units/{civ}/cavalry_javelineer_b - + structures/spartans/civic_center.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_square.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_square.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_square.xml (revision 26000) @@ -1,32 +1,34 @@ 4.0 100 Table Square Table gaia/special_fence.png 20 + 6.0 + props/special/eyecandy/table_square.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple.xml (revision 26000) @@ -1,25 +1,25 @@ kush Temple of Apedemak Pr-ʿIprmk Train Healers and Apedemak Champions and research healing technologies. TempleOfApedemak - + units/{civ}/champion_infantry_apedemak - + structures/kushites/temple.xml structures/fndn_6x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/civil_centre.xml (revision 26000) @@ -1,28 +1,28 @@ 8.0 decay|rubble/rubble_maur_cc maur Rajadhanika - + units/{civ}/infantry_spearman_b units/{civ}/infantry_archer_b units/{civ}/cavalry_javelineer_b units/{civ}/support_elephant - + structures/mauryas/civil_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 26000) @@ -1,65 +1,65 @@ own neutral MercenaryCamp 100 300 100 0 100 12.0 1200 decay|rubble/rubble_stone_5x5 ptol Egyptian Mercenary Camp Stratopedeia Misthophorōn MercenaryCamp Capture this structure to train mercenaries from Hellenistic Egypt. structures/military_settlement.png phase_town 20 0 20 - - - units/{civ}/infantry_spearman_merc_b - units/{civ}/infantry_swordsman_merc_b - units/{civ}/cavalry_spearman_merc_b - units/{civ}/cavalry_javelineer_merc_b - - interface/complete/building/complete_gymnasium.xml 1 + + + units/{civ}/infantry_spearman_merc_b + units/{civ}/infantry_swordsman_merc_b + units/{civ}/cavalry_spearman_merc_b + units/{civ}/cavalry_javelineer_merc_b + + structures/mercenaries/camp_egyptian.xml structures/fndn_7x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/civil_centre.xml (revision 26000) @@ -1,24 +1,26 @@ decay|rubble/rubble_pers_cc pers Provincial Governor Xšaçapāvan - + + + architecture_pers + + + units/{civ}/infantry_spearman_b units/{civ}/infantry_archer_b units/{civ}/cavalry_javelineer_b - - architecture_pers - - + structures/persians/civil_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/dock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/dock.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/dock.xml (revision 26000) @@ -1,23 +1,23 @@ 8.0 ptol Limēn - + units/{civ}/champion_juggernaut - + structures/ptolemies/dock.xml structures/fndn_6x4_dock.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/arch.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/arch.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/arch.xml (revision 26000) @@ -1,47 +1,49 @@ 200 200 400 8.0 2000 decay|rubble/rubble_stone_4x4 rome Triumphal Arch Arcus Triumphālis TriumphalArch structures/arch.png 40 80 + interface/complete/building/complete_theater.xml + structures/romans/triumphal_arch.xml structures/fndn_5x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/tent.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/tent.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/tent.xml (revision 26000) @@ -1,49 +1,51 @@ own neutral enemy 30 50 5.0 5 200 rome Tent Tabernāculum -Village A temporary shelter for soldiers. 10 + interface/complete/building/complete_universal.xml 1 + props/structures/romans/rome_tent.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_colonnade.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_colonnade.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_colonnade.xml (revision 26000) @@ -1,28 +1,30 @@ 4.0 220 Colonnade Corinthian Colonnade gaia/special_fence.png 20 + 8.0 + props/special/eyecandy/sele_colonnade.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_rectangle.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_rectangle.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/table_rectangle.xml (revision 26000) @@ -1,32 +1,34 @@ 4.0 100 Table Rectangle Table gaia/special_fence.png 20 + 6.0 + props/special/eyecandy/table_rectangle.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 26000) @@ -1,143 +1,145 @@ FemaleCitizen 120 180 100 Bow 10 60 1200 2000 100 1.5 50 false Human outline_border.png outline_border_mask.png 0.175 - - 3 - 1 - Soldier - own neutral CivilCentre CivilCentre 200 + + 3 + 1 + Soldier + 2500 5.0 500 500 500 500 8.0 20 0.1 Unit Support Infantry Cavalry 1 1 3000 decay|rubble/rubble_stone_6x6 Civic Center template_structure_civic_civil_centre Build in own or neutral territory. Acquire large tracts of territory. Territory root. Train Citizens and research technologies. Garrison Soldiers for additional arrows. CivCentre Defensive CivilCentre structures/civic_centre.png 100 100 100 20 - - 0.8 - - units/{civ}/support_female_citizen - + phase_town_{civ} phase_city_{civ} unlock_shared_los unlock_shared_dropsites unlock_spies spy_counter archery_tradition hoplite_tradition hellenistic_metropolis - + 5 5 food wood stone metal true interface/complete/building/complete_civ_center.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml attack/weapon/bow_attack.xml attack/impact/arrow_impact.xml true 140 10000 + + 0.8 + + units/{civ}/support_female_citizen + + 90 structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/shrine.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/shrine.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/shrine.xml (revision 26000) @@ -1,23 +1,25 @@ 12.0 decay|rubble/rubble_stone_4x4 kush Shrine ḥwt-nṯr + + structures/kushites/shrine.xml structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/fortress.xml (revision 26000) @@ -1,32 +1,34 @@ 8.0 mace Phrourion Train Heroes. Garrison Soldiers for additional arrows. - + + + silvershields + + + units/{civ}/hero_philip_ii units/{civ}/hero_alexander_iii units/{civ}/hero_demetrius_i - - silvershields - - + structures/macedonians/fortress.xml 24.0 7.6 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml (revision 26000) @@ -1,58 +1,60 @@ structures/maur_pillar Pillar Pillar 75 80 100 100 5.0 1000 decay|rubble/rubble_stone_2x2 maur Edict Pillar of Ashoka Śāsana Stambha Aśokā CivSpecific Pillar structures/ashoka_pillar.png 20 20 + interface/complete/building/complete_iber_monument.xml + props/structures/mauryas/ashoka_pillar.xml structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/apadana.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/apadana.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/apadana.xml (revision 26000) @@ -1,79 +1,81 @@ Palace 300 300 200 8.0 10 3000 decay|rubble/rubble_stone_6x6 pers Throne Hall Apadāna Train Champions and Heroes. ConquestCritical CivSpecific Palace structures/palace.png 60 40 - - 0.8 - - units/{civ}/champion_infantry - units/{civ}/hero_cyrus_ii - units/{civ}/hero_darius_i - units/{civ}/hero_xerxes_i - + immortals - + 1.0 1.0 0.75 0.75 2000 interface/complete/building/complete_broch.xml true 48 40000 + + 0.8 + + units/{civ}/champion_infantry + units/{civ}/hero_cyrus_ii + units/{civ}/hero_darius_i + units/{civ}/hero_xerxes_i + + 40 structures/persians/apadana.xml structures/fndn_8x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/corral.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/corral.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/corral.xml (revision 26000) @@ -1,23 +1,23 @@ ptol Epaulos - + -gaia/fauna_cattle_cow_trainable gaia/fauna_cattle_sanga_trainable - + structures/ptolemies/corral.xml structures/fndn_4x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/military_colony.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/military_colony.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/military_colony.xml (revision 26000) @@ -1,28 +1,28 @@ 12 ptol Klērouchia CivSpecific - + units/{civ}/support_female_citizen units/{civ}/infantry_spearman_merc_b units/{civ}/infantry_swordsman_merc_b units/{civ}/cavalry_spearman_merc_b units/{civ}/cavalry_javelineer_merc_b - + structures/ptolemies/military_colony.xml structures/fndn_9x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/fortress.xml (revision 26000) @@ -1,22 +1,22 @@ rome Castellum Train Heroes. Garrison Soldiers for additional arrows. - + units/{civ}/hero_marcellus units/{civ}/hero_maximus units/{civ}/hero_scipio - + structures/romans/fortress.xml 25.0 8.4 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/military_colony.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/military_colony.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/military_colony.xml (revision 26000) @@ -1,26 +1,26 @@ 8 sele Klērouchia CivSpecific - + units/{civ}/infantry_swordsman_merc_b units/{civ}/infantry_archer_merc_b units/{civ}/cavalry_spearman_merc_b - + structures/seleucids/military_colony.xml structures/fndn_9x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/syssiton.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/syssiton.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/syssiton.xml (revision 26000) @@ -1,67 +1,69 @@ 200 200 200 8.0 10 2000 decay|rubble/rubble_stone_4x6 spart Military Mess Hall Syssition Train Champions and Heroes. ConquestCritical CivSpecific Syssiton structures/syssition.png 40 40 - - 0.7 - - units/{civ}/champion_infantry_spear - units/{civ}/hero_leonidas - units/{civ}/hero_brasidas - units/{civ}/hero_agis - + agoge - + interface/complete/building/complete_gymnasium.xml false 38 40000 + + 0.7 + + units/{civ}/champion_infantry_spear + units/{civ}/hero_leonidas + units/{civ}/hero_brasidas + units/{civ}/hero_agis + + 40 structures/spartans/syssiton.xml structures/fndn_5x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 26000) @@ -1,182 +1,185 @@ land own Structure 500 0.5 5.0 0 10 0 0 0 0 false false 0.0 3.0 9.8 0.85 0.65 0.35 corpse 0 0 true gaia Structure Structure false 0 0 0 0 0 structure true true true true false false false false 0 upright false 0 6 - - 1.0 - - 1.0 - 1.0 - 1.0 - 1.0 - - - + special/rallypoint art/textures/misc/rallypoint_line.png art/textures/misc/rallypoint_line_mask.png 0.25 square round default 2.0 + + + 1.0 + 1.0 + 1.0 + 1.0 + + + 0.0 1 1 10 1 0.0 1 1 10 1 outline_border.png outline_border_mask.png 0.4 interface/complete/building/complete_universal.xml attack/destruction/building_collapse_large.xml interface/alarm/alarm_attackplayer.xml interface/alarm/alarm_attacked_gaia.xml interface/alarm/alarm_attackplayer.xml interface/alarm/alarm_attacked_gaia.xml 6.0 0.6 12.0 20 neutral enemy + + 1.0 + true false false false 4 false false true false Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace/civil_centre.xml (revision 26000) @@ -1,20 +1,20 @@ decay|rubble/rubble_hele_cc mace Agora - + units/{civ}/infantry_pikeman_b units/{civ}/infantry_javelineer_b units/{civ}/cavalry_spearman_b - + structures/macedonians/civic_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/palace.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/palace.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/palace.xml (revision 26000) @@ -1,61 +1,61 @@ 200 200 200 5.0 2000 maur Palace Harmya Territory root. Train Maiden Guards and Heroes. ConquestCritical CivSpecific Palace gaia/special_stoa.png 40 40 - - 0.7 - - units/{civ}/champion_maiden - units/{civ}/champion_maiden_archer - units/{civ}/hero_chanakya - units/{civ}/hero_chandragupta - units/{civ}/hero_ashoka - - interface/complete/building/complete_broch.xml true 48 40000 + + 0.7 + + units/{civ}/champion_maiden + units/{civ}/champion_maiden_archer + units/{civ}/hero_chanakya + units/{civ}/hero_chandragupta + units/{civ}/hero_ashoka + + 40 structures/mauryas/misc_structure_01.xml structures/fndn_9x4.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_fort.xml (revision 26000) @@ -1,23 +1,25 @@ 9.0 Wooden Tower structures/palisade_fort.png + + props/special/palisade_rocks_fort.xml 8.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/civil_centre.xml (revision 26000) @@ -1,27 +1,27 @@ 8.0 ptol Agora - + units/{civ}/infantry_pikeman_b units/{civ}/infantry_slinger_b units/{civ}/cavalry_archer_b units/{civ}/hero_ptolemy_i units/{civ}/hero_ptolemy_iv units/{civ}/hero_cleopatra_vii - + structures/ptolemies/civic_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 26000) @@ -1,64 +1,64 @@ own neutral MercenaryCamp 100 300 100 100 12.0 1200 decay|rubble/rubble_stone_5x5 ptol Mercenary Camp Stratopedeia Misthophorōn MercenaryCamp Cheap Barracks-like structure that is buildable in neutral territory, but casts no territory influence. Train Mercenaries. structures/mercenary_camp.png phase_town 20 0 20 - - - units/{civ}/infantry_spearman_merc_b - units/{civ}/infantry_swordsman_merc_b - units/{civ}/cavalry_spearman_merc_b - units/{civ}/cavalry_javelineer_merc_b - - interface/complete/building/complete_gymnasium.xml 1 + + + units/{civ}/infantry_spearman_merc_b + units/{civ}/infantry_swordsman_merc_b + units/{civ}/cavalry_spearman_merc_b + units/{civ}/cavalry_javelineer_merc_b + + structures/ptolemies/settlement.xml structures/fndn_7x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/civil_centre.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/civil_centre.xml (revision 26000) @@ -1,28 +1,28 @@ 8.0 decay|rubble/rubble_rome_cc rome Forum - + units/{civ}/infantry_swordsman_b units/{civ}/infantry_javelineer_b units/{civ}/cavalry_spearman_b - + structures/romans/civic_centre.xml structures/fndn_9x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele/fortress.xml (revision 26000) @@ -1,26 +1,26 @@ 8.0 sele Phrourion - + parade_of_daphne - + structures/seleucids/fortress.xml 23 7.8 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/gerousia.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/gerousia.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart/gerousia.xml (revision 26000) @@ -1,59 +1,59 @@ Council 200 100 200 8.0 2000 spart Spartan Senate Gerousia Train Heroes. ConquestCritical Council structures/tholos.png 20 40 - - 0.7 - - units/{civ}/hero_leonidas - - interface/complete/building/complete_tholos.xml false 38 40000 + + 0.7 + + units/{civ}/hero_leonidas + + 40 structures/spartans/gerousia.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/uffington_horse.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/uffington_horse.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/uffington_horse.xml (revision 26000) @@ -1,29 +1,31 @@ 10.0 brit Uffington White Horse false false + 8.0 + structures/fndn_stonehenge.xml structures/britons/uffington_horse.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 26000) @@ -1,54 +1,54 @@ - - 1 - own neutral Colony CivilCentre 120 + + 1 + 300 200 200 200 2000 decay|rubble/rubble_stone_5x5 Military Colony template_structure_civic_civil_centre_military_colony Colony structures/military_settlement.png phase_town 40 40 40 - + -phase_town_{civ} -phase_city_{civ} -hellenistic_metropolis - + interface/complete/building/complete_gymnasium.xml 80 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml (revision 26000) @@ -1,36 +1,38 @@ land-shore own neutral Wall 1000 decay|rubble/rubble_stone_2x2 Palisade template_structure_defensive_palisade Wall off an area. Build in own or neutral territory. Palisade gaia/special_palisade.png + 4 25 2 interface/complete/building/complete_wall.xml + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml (revision 26000) @@ -1,81 +1,81 @@ structures/wall_garrisoned own neutral Outpost 50 30 60 13.0 400 decay|rubble/rubble_stone_2x2 Outpost template_structure_defensive_outpost Build in own or neutral territory. Outpost structures/outpost.png 12 - + outpost_vision - + 10 20 1 interface/complete/building/complete_tower.xml 14.0 enemy 0 8 0 90 structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 26000) @@ -1,73 +1,75 @@ structures/temple_heal 200 300 12.0 20 0.1 Unit Support Infantry Cavalry 3 2 2000 decay|rubble/rubble_stone_4x6 Temple template_structure_civic_temple Train Healers and research healing technologies. Town Temple structures/temple.png phase_town 60 - - 0.8 - - units/{civ}/support_healer_b - + heal_range heal_range_2 heal_rate heal_rate_2 garrison_heal health_regen_units - + interface/complete/building/complete_temple.xml false 40 30000 + + 0.8 + + units/{civ}/support_healer_b + + 40 structures/fndn_4x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 26000) @@ -1,72 +1,74 @@ 30 75 5.0 3 0 0.1 Unit Support+!Elephant 1 800 decay|rubble/rubble_stone_2x2 House template_structure_civic_house Village House structures/house.png phase_village 15 5 - - - units/{civ}/support_female_citizen_house - + health_females_01 pop_house_01 pop_house_02 unlock_females_house - + interface/complete/building/complete_house.xml 8.0 false 16 65535 + + + units/{civ}/support_female_citizen_house + + 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml (revision 26000) @@ -1,79 +1,79 @@ Stone 90 0 80 40 15 4500 5000 40 6 9.81 false props/units/weapons/tower_artillery_projectile.xml props/units/weapons/tower_artillery_projectile_impact.xml 0.3 -Human !Organic 1 0 200 200 200 15.0 5 1400 Artillery Tower template_structure_defensive_tower_artillery ArtilleryTower structures/tower_artillery.png phase_city 40 40 - + tower_health - + attack/impact/siegeprojectilehit.xml attack/siege/ballist_attack.xml false 32 30000 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 26000) @@ -1,54 +1,54 @@ 15 150 100 100 15.0 5 1000 Stone Tower template_structure_defensive_tower_stone Garrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot. StoneTower structures/defense_tower.png phase_town 20 20 - + tower_watch tower_crenellations tower_range tower_murderholes tower_health - + false 32 30000 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 26000) @@ -1,78 +1,78 @@ FemaleCitizen 50 100 100 40 100 8.0 800 decay|rubble/rubble_stone_3x3 Storehouse template_structure_economic_storehouse Research gathering technologies. DropsiteWood DropsiteMetal DropsiteStone Village Storehouse structures/storehouse.png phase_village 20 - + + gather_lumbering_ironaxes gather_lumbering_strongeraxes gather_lumbering_sharpaxes gather_mining_servants gather_mining_serfs gather_mining_slaves gather_mining_wedgemallet gather_mining_shaftmining gather_mining_silvermining gather_capacity_basket gather_capacity_wheelbarrow gather_capacity_carts - - + wood stone metal true interface/complete/building/complete_storehouse.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml false 20 30000 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml (revision 26000) @@ -1,63 +1,63 @@ structures/xp_trickle 180 200 200 8.0 5 Elephant 3000 decay|rubble/rubble_stone_6x6 Elephant Stable template_structure_military_elephant_stable Train Elephants and research Elephant technologies. City ElephantStable phase_city structures/stable_elephant.png 40 40 - - 0.7 - - units/{civ}/support_elephant - units/{civ}/elephant_archer_b - units/{civ}/champion_elephant - - interface/complete/building/complete_elephant_stable.xml 38 + + 0.7 + + units/{civ}/support_elephant + units/{civ}/elephant_archer_b + units/{civ}/champion_elephant + + 40 structures/fndn_9x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml (revision 26000) @@ -1,55 +1,55 @@ 120 200 12.0 10 Infantry 2000 decay|rubble/rubble_stone_5x5 Practice Range template_structure_military_range Train Ranged Infantry and research technologies. Village Range structures/range.png phase_village 40 - - 0.8 - - units/{civ}/infantry_javelineer_b - units/{civ}/infantry_slinger_b - units/{civ}/infantry_archer_b - - interface/complete/building/complete_range.xml + + 0.8 + + units/{civ}/infantry_javelineer_b + units/{civ}/infantry_slinger_b + units/{civ}/infantry_archer_b + + 32 structures/fndn_7x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_amphitheater.xml (revision 26000) @@ -1,54 +1,54 @@ 500 500 500 20.0 2000 Amphitheater template_structure_special_amphitheater Amphitheater structures/theater.png 100 100 - - 0.8 - - units/{civ}/champion_infantry_spear_gladiator - units/{civ}/champion_infantry_sword_gladiator - - interface/complete/building/complete_tholos.xml false 100 40000 + + 0.8 + + units/{civ}/champion_infantry_spear_gladiator + units/{civ}/champion_infantry_sword_gladiator + + 40 structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 26000) @@ -1,94 +1,94 @@ structures/wonder_population_cap Wonder 4 1000 1000 1500 1000 10.0 50 0.1 Unit Support Soldier 5 2 5000 decay|rubble/rubble_stone_6x6 Wonder template_structure_wonder Bring glory to your civilization and add large tracts of land to your empire. ConquestCritical City Wonder structures/wonder.png phase_city 200 300 200 - + wonder_population_cap - + 15 25 3 1.0 1.0 1.0 1.0 2000 interface/complete/building/complete_wonder.xml true 100 65535 72 structures/fndn_9x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 26000) @@ -1,64 +1,64 @@ 9 3 40 100 9.0 3 400 Sentry Tower template_structure_defensive_tower_sentry Garrison Infantry for additional arrows. Needs the “Murder Holes” technology to protect its foot. SentryTower structures/sentry_tower.png phase_village 20 - + tower_watch - + false 16 30000 structures/{civ}/defense_tower Reinforce with stone and upgrade to a defense tower. phase_town 50 100 upgrading Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 26000) @@ -1,73 +1,75 @@ Trader+!Ship -1 -1 100 150 300 8.0 1500 decay|rubble/rubble_stone_5x5 Market template_structure_economic_market Barter resources. Establish trade routes. Train Traders and research trade and barter technologies. Barter Trade Town Market structures/market.png phase_town 60 land 0.2 - - 0.7 + trader_health trade_gain_01 trade_gain_02 trade_commercial_treaty - - units/{civ}/support_trader - - + interface/complete/building/complete_market.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml false 40 30000 + + 0.7 + + units/{civ}/support_trader + + 32 structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 26000) @@ -1,84 +1,86 @@ own ally neutral shore Dock 150 200 8.0 2500 decay|rubble/rubble_stone_4x4 Dock template_structure_military_dock Build upon a shoreline in own, neutral, or allied territory. Establish trade routes. Construct Ships and research Ship technologies. Economic Naval Trade Village Dock structures/dock.png 40 land naval 0.2 true 0.0 - - 0.8 - - units/{civ}/ship_fishing - units/{civ}/ship_merchant - units/{civ}/ship_bireme - units/{civ}/ship_trireme - units/{civ}/ship_quinquereme - units/{civ}/ship_fire - + + ship + + fishing_boat_gather_rate fishing_boat_gather_capacity ship_cost_time ship_health ship_movement_speed equine_transports - - - ship - + food wood stone metal true interface/complete/building/complete_dock.xml + + 0.8 + + units/{civ}/ship_fishing + units/{civ}/ship_merchant + units/{civ}/ship_bireme + units/{civ}/ship_trireme + units/{civ}/ship_quinquereme + units/{civ}/ship_fire + + 40 structures/fndn_4x4_dock.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 26000) @@ -1,106 +1,108 @@ Bow 10 60 1200 2000 100 1.5 50 false Human outline_border.png outline_border_mask.png 0.175 - - 4 - 1 - Soldier - Fortress Fortress 80 + + 4 + 1 + Soldier + 8 10.0 450 300 600 8.0 20 0.075 Support Infantry Cavalry Siege 6 5200 decay|rubble/rubble_stone_6x6 Fortress template_structure_military_fortress Garrison Soldiers for additional arrows. GarrisonFortress Defensive Fortress structures/fortress.png phase_city 60 120 - - 0.8 + attack_soldiers_will - + interface/complete/building/complete_fortress.xml attack/weapon/bow_attack.xml attack/impact/arrow_impact.xml 2 80 + + 0.8 + 90 structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 26000) @@ -1,71 +1,73 @@ 50 100 2.0 250 decay|rubble/rubble_field Field template_structure_resource_field Field Harvest grain for food. Each subsequent gatherer works less efficiently. structures/field.png 50 false false + 15 40 5 false Infinity food.grain 5 0.90 interface/complete/building/complete_field.xml 8.0 + 0 structures/plot_field_found.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_theater.xml (revision 26000) @@ -1,53 +1,55 @@ structures/theater Theater 500 200 600 200 10.0 3000 Theater template_structure_special_theater Theater structures/theater.png 40 120 40 + interface/complete/building/complete_greek_theater.xml false 100 40000 + 40 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/ship_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/ship_trireme.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/ship_trireme.xml (revision 26000) @@ -1,31 +1,25 @@ 8.0 pers persian Phoenician Trireme Hamaraniyanava Vazarka units/pers_ship_trireme.png - + + 0.8 units/pers/cavalry_axeman_b_trireme units/pers/cavalry_javelineer_b_trireme - - 1.0 - 1.0 - 1.0 - 1.0 - - - + structures/persians/trireme.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml (revision 26000) @@ -1,76 +1,76 @@ Bolt 100 90 30 15 500 4000 150 1 9.81 false props/units/weapons/tower_artillery_projectile_impact.xml 0.1 1 0 200 200 100 15.0 5 1400 Bolt Tower template_structure_defensive_tower_bolt BoltTower structures/tower_bolt.png phase_city 40 20 - + tower_health - + attack/weapon/arrowfly.xml attack/impact/arrow_metal.xml false 32 30000 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 26000) @@ -1,70 +1,70 @@ FemaleCitizen 50 100 100 300 45 100 8.0 900 decay|rubble/rubble_stone_4x4 Farmstead template_structure_economic_farmstead Research food gathering technologies. DropsiteFood Village Farmstead structures/farmstead.png phase_village 20 - + + gather_wicker_baskets gather_farming_plows gather_farming_training gather_farming_fertilizer gather_farming_harvester - - + food true interface/complete/building/complete_farmstead.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml false 20 30000 20 structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 26000) @@ -1,79 +1,81 @@ structures/xp_trickle 150 200 100 12.0 10 Infantry Cavalry 2000 decay|rubble/rubble_stone_4x4 Barracks template_structure_military_barracks Train Infantry and research Infantry technologies. Village Barracks structures/barracks.png phase_village 40 20 - + + + barracks_batch_training + infantry_cost_time + unlock_champion_infantry + pair_unlock_champions_sele + + + + + + interface/complete/building/complete_barracks.xml + + + 0.8 units/{civ}/infantry_spearman_b units/{civ}/infantry_pikeman_b units/{civ}/infantry_maceman_b units/{civ}/infantry_axeman_b units/{civ}/infantry_swordsman_b units/{civ}/infantry_javelineer_b units/{civ}/infantry_slinger_b units/{civ}/infantry_archer_b units/{civ}/champion_infantry_spearman units/{civ}/champion_infantry_pikeman units/{civ}/champion_infantry_maceman units/{civ}/champion_infantry_axeman units/{civ}/champion_infantry_swordsman units/{civ}/champion_infantry_javelineer units/{civ}/champion_infantry_slinger units/{civ}/champion_infantry_archer - - barracks_batch_training - infantry_cost_time - unlock_champion_infantry - pair_unlock_champions_sele - - - - - - interface/complete/building/complete_barracks.xml - - + 32 structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml (revision 26000) @@ -1,70 +1,70 @@ 120 200 12.0 1 Infantry Healer 2000 decay|rubble/rubble_stone_4x4 Forge template_structure_military_forge Research attack damage and damage resistance technologies. -ConquestCritical Town Forge structures/blacksmith.png phase_town 40 - + soldier_attack_melee_01 soldier_attack_melee_02 soldier_attack_melee_03 soldier_attack_melee_03_variant soldier_attack_ranged_01 soldier_attack_ranged_02 soldier_attack_ranged_03 soldier_resistance_hack_01 soldier_resistance_hack_02 soldier_resistance_hack_03 soldier_resistance_pierce_01 soldier_resistance_pierce_02 soldier_resistance_pierce_03 archer_attack_spread - + interface/complete/building/complete_forge.xml 38 30000 32 structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 26000) @@ -1,67 +1,69 @@ 50 100 5.0 500 decay|rubble/rubble_stone_3x3 Corral template_structure_resource_corral Raise Domestic Animals for food. Economic Village Corral structures/corral.png phase_village 20 - - 0.7 - - gaia/fauna_goat_trainable - gaia/fauna_sheep_trainable - gaia/fauna_pig_trainable - gaia/fauna_cattle_cow_trainable - + gather_animals_stockbreeding - + 20 interface/complete/building/complete_corral.xml false 20 30000 + + 0.7 + + gaia/fauna_goat_trainable + gaia/fauna_sheep_trainable + gaia/fauna_pig_trainable + gaia/fauna_cattle_cow_trainable + + 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_rotarymill.xml (revision 26000) @@ -1,57 +1,59 @@ 100 200 100 6.0 2000 Rotary Mill template_structure_special_rotarymill RotaryMill structures/rotarymill.png 40 20 + food true interface/complete/building/complete_ffactri.xml 8.0 false 32 40000 + 40 structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/hero_cyrus_ii.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/hero_cyrus_ii.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/hero_cyrus_ii.xml (revision 26000) @@ -1,29 +1,23 @@ units/heroes/pers_hero_cyrus_ii pers persian Cyrus II The Great Kuruš units/pers_hero_cyrus.png - + + 0.7 units/pers/champion_infantry - - 1.0 - 1.0 - 1.0 - 1.0 - - - + units/persians/hero_cavalry_spearman_cyrus_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 26000) @@ -1,37 +1,39 @@ land-shore Wall 8.0 Wall template_structure_defensive_wall Wall off your town for a stout defense. Wall structures/wall.png phase_town 4.5 + interface/complete/building/complete_wall.xml false 20 65535 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml (revision 26000) @@ -1,73 +1,75 @@ structures/arsenal_repair 180 300 12.0 2 Siege 2000 decay|rubble/rubble_stone_5x5 Arsenal template_structure_military_arsenal Train Champion Infantry Crossbowmen, construct Siege Engines, and research Siege Engine technologies. City Arsenal structures/siege_workshop.png phase_city 60 - - 0.7 - - units/{civ}/champion_infantry_crossbowman - units/{civ}/siege_scorpio_packed - units/{civ}/siege_polybolos_packed - units/{civ}/siege_oxybeles_packed - units/{civ}/siege_lithobolos_packed - units/{civ}/siege_ballista_packed - units/{civ}/siege_ram - units/{civ}/siege_tower - + siege_attack siege_cost_time siege_health siege_pack_unpack siege_bolt_accuracy - + interface/complete/building/complete_barracks.xml 38 + + 0.7 + + units/{civ}/champion_infantry_crossbowman + units/{civ}/siege_scorpio_packed + units/{civ}/siege_polybolos_packed + units/{civ}/siege_oxybeles_packed + units/{civ}/siege_lithobolos_packed + units/{civ}/siege_ballista_packed + units/{civ}/siege_ram + units/{civ}/siege_tower + + 40 structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml (revision 26000) @@ -1,55 +1,55 @@ Embassy 150 12.0 6 Support Infantry Cavalry 2000 decay|rubble/rubble_stone_3x3 Embassy template_structure_military_embassy Town Embassy phase_town - - 0.8 - 30 interface/complete/building/complete_gymnasium.xml 25 + + 0.8 + 24 structures/fndn_4x4.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml (revision 26000) @@ -1,71 +1,73 @@ structures/xp_trickle 120 200 50 12.0 10 Cavalry 2000 decay|rubble/rubble_stone_5x5 Stable template_structure_military_stable Train Cavalry and research Cavalry technologies. Village Stable structures/stable.png phase_village 40 10 - - 0.8 - - units/{civ}/cavalry_axeman_b - units/{civ}/cavalry_swordsman_b - units/{civ}/cavalry_spearman_b - units/{civ}/cavalry_javelineer_b - units/{civ}/cavalry_archer_b - units/{civ}/champion_cavalry - units/{civ}/champion_chariot - units/{civ}/war_dog - + stable_batch_training cavalry_cost_time cavalry_movement_speed cavalry_health nisean_horses unlock_champion_cavalry unlock_champion_chariots - + interface/complete/building/complete_stable.xml + + 0.8 + + units/{civ}/cavalry_axeman_b + units/{civ}/cavalry_swordsman_b + units/{civ}/cavalry_spearman_b + units/{civ}/cavalry_javelineer_b + units/{civ}/cavalry_archer_b + units/{civ}/champion_cavalry + units/{civ}/champion_chariot + units/{civ}/war_dog + + 32 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special_library.xml (revision 26000) @@ -1,55 +1,57 @@ structures/library Library 200 200 200 9.0 2000 decay|rubble/rubble_stone_4x6 Library template_structure_special_library Library structures/library_scroll.png 40 40 + interface/complete/building/complete_library.xml false 50 40000 + 40 structures/fndn_7x9.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/ship_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/ship_trireme.xml (revision 25999) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/ship_trireme.xml (revision 26000) @@ -1,29 +1,23 @@ 8.0 Triḗrēs Athēnaía Athenian Trireme units/hele_ship_trireme.png - + + 0.7 units/athen/infantry_marine_archer_b units/athen/champion_marine - - 1.0 - 1.0 - 1.0 - 1.0 - - - + structures/athenians/trireme.xml