Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 27245) @@ -1,638 +1,638 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ "Code", "Culture", "Music", "CivBonuses", "StartEntities", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); if (selectableOnly && !data.SelectableInGameSetup) continue; const template = Engine.GetTemplate("special/players/" + data.Code); data.Name = template.Identity.GenericName; data.Emblem = "session/portraits/" + template.Identity.Icon; data.History = template.Identity.History; civData[data.Code] = data; } return civData; } /** * @return {string[]} - All the classes for this identity template. */ function GetIdentityClasses(template) { let classString = ""; if (template.Classes && template.Classes._string) classString += " " + template.Classes._string; if (template.VisibleClasses && template.VisibleClasses._string) classString += " " + template.VisibleClasses._string; if (template.Rank) classString += " " + template.Rank; return classString.length > 1 ? classString.substring(1).split(" ") : []; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : []; } /** * Check if a given list of classes matches another list of classes. * Useful f.e. for checking identity classes. * * @param classes - List of the classes to check against. * @param match - Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2. * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed. * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed. * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise. */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // Transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (let sublist of match) { // If the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) || (c[0] != "!" && classes.indexOf(c) != -1))) return true; } return false; } /** * Gets the value originating at the value_path as-is, with no modifiers applied. * * @param {Object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {number} default_value - A value to use if one is not specified in the template. * @return {number} */ function GetBaseTemplateDataValue(template, value_path, default_value) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || default_value; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {Object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {Object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @param {number} default_value - A value to use if one is not specified in the template. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}, default_value) { let current_value = GetBaseTemplateDataValue(template, value_path, default_value); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers && modifiers[mod_key]) current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {Object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {Object} resources - An instance of the Resources class. * @param {Object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, resources, modifiers = {}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. // @param {number} default_value - A value to use if one is not specified in the template. const getEntityValue = function(value_path, mod_key, default_value = 0) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers, default_value); }; let ret = {}; if (template.Resistance) { // Don't show Foundation resistance. ret.resistance = {}; if (template.Resistance.Entity) { if (template.Resistance.Entity.Damage) { ret.resistance.Damage = {}; for (let damageType in template.Resistance.Entity.Damage) ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType); } if (template.Resistance.Entity.Capture) ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture"); if (template.Resistance.Entity.ApplyStatus) { ret.resistance.ApplyStatus = {}; for (let statusEffect in template.Resistance.Entity.ApplyStatus) ret.resistance.ApplyStatus[statusEffect] = { "blockChance": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/BlockChance"), "duration": getEntityValue("Resistance/Entity/ApplyStatus/" + statusEffect + "/Duration") }; } } } let getAttackEffects = (temp, path) => { let effects = {}; if (temp.Capture) effects.Capture = getEntityValue(path + "/Capture"); if (temp.Damage) { effects.Damage = {}; for (let damageType in temp.Damage) effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType); } if (temp.ApplyStatus) effects.ApplyStatus = temp.ApplyStatus; return effects; }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; ret.attack[type] = { "attackName": { "name": template.Attack[type].AttackName._string || template.Attack[type].AttackName, "context": template.Attack[type].AttackName["@context"] }, "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "yOrigin": getAttackStat("Origin/Y") }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].yOrigin + ret.attack[type].maxRange)); ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Projectile) ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true"; Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); if (template.Attack[type].Splash) { ret.attack[type].splash = { "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, }; Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", }; Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) ret.auras[auraID] = GetAuraDataHelper(auraTemplates[auraID]); } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) { ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category }; if (template.TrainingRestrictions.MatchLimit) ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit; } if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.Garrisonable) ret.garrisonable = { "size": getEntityValue("Garrisonable/Size") }; if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "health": getEntityValue("Heal/Health"), "range": getEntityValue("Heal/Range"), "interval": getEntityValue("Heal/Interval") }; if (template.ResourceGatherer) { ret.resourceGatherRates = {}; let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed"); for (let type in template.ResourceGatherer.Rates) ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed; } if (template.ResourceDropsite) ret.resourceDropsite = { "types": template.ResourceDropsite.Types.split(" ") }; if (template.ResourceTrickle) { ret.resourceTrickle = { "interval": +template.ResourceTrickle.Interval, "rates": {} }; for (let type in template.ResourceTrickle.Rates) ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type); } if (template.Loot) { ret.loot = {}; for (let type in template.Loot) ret.loot[type] = getEntityValue("Loot/"+ type); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else ret.obstruction.shape.type = "cluster"; } if (template.Pack) ret.pack = { "state": template.Pack.State, "time": getEntityValue("Pack/Time"), }; if (template.Population && template.Population.Bonus) ret.population = { "bonus": getEntityValue("Population/Bonus") }; if (template.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; - ret.requiredTechnology = template.Identity.RequiredTechnology; + ret.requirements = template.Identity.Requirements; 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 + "icon": upgrade.Icon, + "requirements": upgrade.Requirements }); } } if (template.Researcher) { ret.techCostMultiplier = {}; for (const res of resources.GetCodes().concat(["time"])) ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res, null, 1); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.Treasure) { ret.treasure = { "collectTime": getEntityValue("Treasure/CollectTime"), "resources": {} }; for (let resource in template.Treasure.Resources) ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource); } if (template.TurretHolder) ret.turretHolder = { "turretPoints": template.TurretHolder.TurretPoints }; if (template.Upkeep) { ret.upkeep = { "interval": +template.Upkeep.Interval, "rates": {} }; for (let type in template.Upkeep.Rates) ret.upkeep.rates[type] = getEntityValue("Upkeep/Rates/" + type); } if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "/fortress", "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap }; if (template.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(/\s+/); } if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length, "angle": +(template.WallPiece.Orientation || 1) * Math.PI, "indent": +(template.WallPiece.Indent || 0), "bend": +(template.WallPiece.Bend || 0) * Math.PI }; return ret; } /** * Get basic information about a technology template. * @param {Object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects, "replaces": template.replaces }; } /** * Get information about a technology template. * @param {Object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. * @param {Object} resources - An instance of the Resources class. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } /** * Get information about an aura template. * @param {object} template - A valid template as obtained by loading the aura JSON file. */ function GetAuraDataHelper(template) { return { "name": { "generic": template.auraName, }, "description": template.auraDescription || null, "modifications": template.modifications, "radius": template.radius || null, }; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } /** * Remove filter prefix (mirage, corpse, etc) from template name. * * ie. filter|dir/to/template -> dir/to/template */ function removeFiltersFromTemplateName(templateName) { return templateName.split("|").pop(); } Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 27245) @@ -1,1235 +1,1238 @@ var g_TooltipTextFormats = { "unit": { "font": "sans-10", "color": "orange" }, "header": { "font": "sans-bold-13" }, "body": { "font": "sans-13" }, "comma": { "font": "sans-12" }, "namePrimaryBig": { "font": "sans-bold-16" }, "namePrimarySmall": { "font": "sans-bold-12" }, "nameSecondary": { "font": "sans-bold-16" } }; var g_SpecificNamesPrimary = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 2; var g_ShowSecondaryNames = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 1; function initDisplayedNames() { registerConfigChangeHandler(changes => { if (changes.has("gui.session.howtoshownames")) updateDisplayedNames(); }); } /** * String of four spaces to be used as indentation in gui strings. */ var g_Indent = " "; var g_DamageTypesMetadata = new DamageTypesMetadata(); var g_StatusEffectsMetadata = new StatusEffectsMetadata(); /** * If true, always shows whether the splash damage deals friendly fire. * Otherwise display the friendly fire tooltip only if it does. */ var g_AlwaysDisplayFriendlyFire = false; function getCostTypes() { return g_ResourceData.GetCodes().concat(["population", "time"]); } function resourceIcon(resource) { return '[icon="icon_' + resource + '"]'; } function resourceNameFirstWord(type) { return translateWithContext("firstWord", g_ResourceData.GetNames()[type]); } function resourceNameWithinSentence(type) { return translateWithContext("withinSentence", g_ResourceData.GetNames()[type]); } /** * Format resource amounts to proper english and translate (for example: "200 food, 100 wood and 300 metal"). */ function getLocalizedResourceAmounts(resources) { let amounts = g_ResourceData.GetCodes() .filter(type => !!resources[type]) .map(type => sprintf(translate("%(amount)s %(resourceType)s"), { "amount": resources[type], "resourceType": resourceNameWithinSentence(type) })); if (amounts.length < 2) return amounts.join(); let lastAmount = amounts.pop(); return sprintf(translate("%(previousAmounts)s and %(lastAmount)s"), { // Translation: This comma is used for separating first to penultimate elements in an enumeration. "previousAmounts": amounts.join(translate(", ")), "lastAmount": lastAmount }); } function bodyFont(text) { return setStringTags(text, g_TooltipTextFormats.body); } function headerFont(text) { return setStringTags(text, g_TooltipTextFormats.header); } function unitFont(text) { return setStringTags(text, g_TooltipTextFormats.unit); } function commaFont(text) { return setStringTags(text, g_TooltipTextFormats.comma); } function getSecondsString(seconds) { return sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", seconds), { "time": seconds, "second": unitFont(translatePlural("second", "seconds", seconds)) }); } /** * Entity templates have a `Tooltip` tag in the Identity component. * (The contents of which are copied to a `tooltip` attribute in globalscripts.) * * Technologies have a `tooltip` attribute. */ function getEntityTooltip(template) { if (!template.tooltip) return ""; return bodyFont(template.tooltip); } /** * Technologies have a `description` attribute, and Auras have an `auraDescription` * attribute, which becomes `description`. * * (For technologies, this happens in globalscripts.) * * (For auras, this happens either in the Auras component (for session gui) or * reference/common/load.js (for Reference Suite gui)) */ function getDescriptionTooltip(template) { if (!template.description) return ""; return bodyFont(template.description); } /** * Entity templates have a `History` tag in the Identity component. * (The contents of which are copied to a `history` attribute in globalscripts.) */ function getHistoryTooltip(template) { if (!template.history) return ""; return bodyFont(template.history); } function getHealthTooltip(template) { if (!template.health) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Health:")), "details": template.health }); } function getCurrentHealthTooltip(entState, label) { if (!entState.maxHitpoints) return ""; return sprintf(translate("%(healthLabel)s %(current)s / %(max)s"), { "healthLabel": headerFont(label || translate("Health:")), "current": Math.round(entState.hitpoints), "max": Math.round(entState.maxHitpoints) }); } function getCurrentCaptureTooltip(entState, label) { if (!entState.maxCapturePoints) return ""; return sprintf(translate("%(captureLabel)s %(current)s / %(max)s"), { "captureLabel": headerFont(label || translate("Capture points:")), "current": Math.round(entState.capturePoints[entState.player]), "max": Math.round(entState.maxCapturePoints) }); } /** * Converts an resistance level into the actual reduction percentage. */ function resistanceLevelToPercentageString(level) { return sprintf(translate("%(percentage)s%%"), { "percentage": (100 - Math.round(Math.pow(0.9, level) * 100)) }); } function getResistanceTooltip(template) { if (!template.resistance) return ""; let details = []; if (template.resistance.Damage) details.push(getDamageResistanceTooltip(template.resistance.Damage)); if (template.resistance.Capture) details.push(getCaptureResistanceTooltip(template.resistance.Capture)); if (template.resistance.ApplyStatus) details.push(getStatusEffectsResistanceTooltip(template.resistance.ApplyStatus)); return details.length ? sprintf(translate("%(label)s\n%(details)s"), { "label": headerFont(translate("Resistance:")), "details": g_Indent + details.join("\n" + g_Indent) }) : ""; } function getDamageResistanceTooltip(resistanceTypeTemplate) { if (!resistanceTypeTemplate) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Damage:")), "details": g_DamageTypesMetadata.sort(Object.keys(resistanceTypeTemplate)).map( dmgType => sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), { "damage": resistanceTypeTemplate[dmgType].toFixed(1), "damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType))), "resistancePercentage": '[font="sans-10"]' + sprintf(translate("(%(resistancePercentage)s)"), { "resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate[dmgType]) }) + '[/font]' }) ).join(commaFont(translate(", "))) }); } function getCaptureResistanceTooltip(resistanceTypeTemplate) { if (!resistanceTypeTemplate) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Capture:")), "details": sprintf(translate("%(damage)s %(damageType)s %(resistancePercentage)s"), { "damage": resistanceTypeTemplate.toFixed(1), "damageType": unitFont(translateWithContext("damage type", "Capture")), "resistancePercentage": '[font="sans-10"]' + sprintf(translate("(%(resistancePercentage)s)"), { "resistancePercentage": resistanceLevelToPercentageString(resistanceTypeTemplate) }) + '[/font]' }) }); } function getStatusEffectsResistanceTooltip(resistanceTypeTemplate) { if (!resistanceTypeTemplate) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Status Effects:")), "details": Object.keys(resistanceTypeTemplate).map( statusEffect => { if (resistanceTypeTemplate[statusEffect].blockChance == 1) return sprintf(translate("Blocks %(name)s"), { "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))) }); if (resistanceTypeTemplate[statusEffect].blockChance == 0) return sprintf(translate("%(name)s %(details)s"), { "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))), "details": sprintf(translate("Duration reduction: %(durationReduction)s%%"), { "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100) }) }); if (resistanceTypeTemplate[statusEffect].duration == 1) return sprintf(translate("%(name)s %(details)s"), { "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))), "details": sprintf(translate("Blocks: %(blockPercentage)s%%"), { "blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100 }) }); return sprintf(translate("%(name)s %(details)s"), { "name": unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(statusEffect))), "details": sprintf(translate("Blocks: %(blockPercentage)s%%, Duration reduction: %(durationReduction)s%%"), { "blockPercentage": resistanceTypeTemplate[statusEffect].blockChance * 100, "durationReduction": (100 - resistanceTypeTemplate[statusEffect].duration * 100) }) }); } ).join(commaFont(translate(", "))) }); } function attackRateDetails(interval, projectiles) { if (!interval) return ""; if (projectiles === 0) return translate("Garrison to fire arrows"); let attackRateString = getSecondsString(interval / 1000); let header = headerFont(translate("Interval:")); if (projectiles && +projectiles > 1) { header = headerFont(translate("Rate:")); let projectileString = sprintf(translatePlural("%(projectileCount)s %(projectileName)s", "%(projectileCount)s %(projectileName)s", projectiles), { "projectileCount": projectiles, "projectileName": unitFont(translatePlural("arrow", "arrows", projectiles)) }); attackRateString = sprintf(translate("%(projectileString)s / %(attackRateString)s"), { "projectileString": projectileString, "attackRateString": attackRateString }); } return sprintf(translate("%(label)s %(details)s"), { "label": header, "details": attackRateString }); } function rangeDetails(attackTypeTemplate) { if (!attackTypeTemplate.maxRange) return ""; let rangeTooltipString = { "relative": { // Translation: For example: Range: 2 to 10 (+2) meters "minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"), // Translation: For example: Range: 10 (+2) meters "no-minRange": translate("%(rangeLabel)s %(maxRange)s (%(relativeRange)s) %(rangeUnit)s"), }, "non-relative": { // Translation: For example: Range: 2 to 10 meters "minRange": translate("%(rangeLabel)s %(minRange)s to %(maxRange)s %(rangeUnit)s"), // Translation: For example: Range: 10 meters "no-minRange": translate("%(rangeLabel)s %(maxRange)s %(rangeUnit)s"), } }; let minRange = Math.round(attackTypeTemplate.minRange); let maxRange = Math.round(attackTypeTemplate.maxRange); let realRange = attackTypeTemplate.elevationAdaptedRange; let relativeRange = realRange ? Math.round(realRange - maxRange) : 0; return sprintf(rangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], { "rangeLabel": headerFont(translate("Range:")), "minRange": minRange, "maxRange": maxRange, "relativeRange": relativeRange > 0 ? sprintf(translate("+%(number)s"), { "number": relativeRange }) : relativeRange, "rangeUnit": unitFont(minRange || relativeRange ? // Translation: For example "0.5 to 1 meters", "1 (+1) meters" or "1 to 2 (+3) meters" translate("meters") : translatePlural("meter", "meters", maxRange)) }); } function damageDetails(damageTemplate) { if (!damageTemplate) return ""; return g_DamageTypesMetadata.sort(Object.keys(damageTemplate).filter(dmgType => damageTemplate[dmgType])).map( dmgType => sprintf(translate("%(damage)s %(damageType)s"), { "damage": (+damageTemplate[dmgType]).toFixed(1), "damageType": unitFont(translateWithContext("damage type", g_DamageTypesMetadata.getName(dmgType))) })).join(commaFont(translate(", "))); } function captureDetails(captureTemplate) { if (!captureTemplate) return ""; return sprintf(translate("%(amount)s %(name)s"), { "amount": (+captureTemplate).toFixed(1), "name": unitFont(translateWithContext("damage type", "Capture")) }); } function splashDetails(splashTemplate) { let splashLabel = sprintf(headerFont(translate("%(splashShape)s Splash")), { "splashShape": translate(splashTemplate.shape) }); let splashDamageTooltip = sprintf(translate("%(label)s: %(effects)s"), { "label": splashLabel, "effects": attackEffectsDetails(splashTemplate) }); if (g_AlwaysDisplayFriendlyFire || splashTemplate.friendlyFire) splashDamageTooltip += commaFont(translate(", ")) + sprintf(translate("Friendly Fire: %(enabled)s"), { "enabled": splashTemplate.friendlyFire ? translate("Yes") : translate("No") }); return splashDamageTooltip; } function applyStatusDetails(applyStatusTemplate) { if (!applyStatusTemplate) return ""; return sprintf(translate("gives %(name)s"), { "name": Object.keys(applyStatusTemplate).map(x => unitFont(translateWithContext("status effect", g_StatusEffectsMetadata.getName(x))) ).join(commaFont(translate(", "))), }); } function attackEffectsDetails(attackTypeTemplate) { if (!attackTypeTemplate) return ""; let effects = [ captureDetails(attackTypeTemplate.Capture || undefined), damageDetails(attackTypeTemplate.Damage || undefined), applyStatusDetails(attackTypeTemplate.ApplyStatus || undefined) ]; return effects.filter(effect => effect).join(commaFont(translate(", "))); } function getAttackTooltip(template) { if (!template.attack) return ""; let tooltips = []; for (let attackType in template.attack) { // Slaughter is used to kill animals, so do not show it. if (attackType == "Slaughter") continue; let attackTypeTemplate = template.attack[attackType]; let attackLabel = sprintf(headerFont(translate("%(attackType)s")), { "attackType": translateWithContext(attackTypeTemplate.attackName.context || "Name of an attack, usually the weapon.", attackTypeTemplate.attackName.name) }); let projectiles; // Use either current rate from simulation or default count if the sim is not running. // TODO: This ought to be extended to include units which fire multiple projectiles. if (template.buildingAI) projectiles = template.buildingAI.arrowCount || template.buildingAI.defaultArrowCount; let splashTemplate = attackTypeTemplate.splash; // Show the effects of status effects below. let statusEffectsDetails = []; if (attackTypeTemplate.ApplyStatus) for (let status in attackTypeTemplate.ApplyStatus) statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status, attackTypeTemplate.ApplyStatus[status], true)); statusEffectsDetails = statusEffectsDetails.join(""); tooltips.push(sprintf(translate("%(attackLabel)s: %(effects)s, %(range)s, %(rate)s%(statusEffects)s%(splash)s"), { "attackLabel": attackLabel, "effects": attackEffectsDetails(attackTypeTemplate), "range": rangeDetails(attackTypeTemplate), "rate": attackRateDetails(attackTypeTemplate.repeatTime, projectiles), "splash": splashTemplate ? "\n" + g_Indent + g_Indent + splashDetails(splashTemplate) : "", "statusEffects": statusEffectsDetails })); } return sprintf(translate("%(label)s\n%(details)s"), { "label": headerFont(translate("Attack:")), "details": g_Indent + tooltips.join("\n" + g_Indent) }); } /** * @param applier - if true, return the tooltip for the Applier. If false, Receiver is returned. */ function getStatusEffectsTooltip(statusCode, template, applier) { let tooltipAttributes = []; let statusData = g_StatusEffectsMetadata.getData(statusCode); if (template.Damage || template.Capture) tooltipAttributes.push(attackEffectsDetails(template)); if (template.Interval) tooltipAttributes.push(attackRateDetails(+template.Interval)); if (template.Duration) tooltipAttributes.push(getStatusEffectDurationTooltip(template)); if (applier && statusData.applierTooltip) tooltipAttributes.push(translateWithContext("status effect", statusData.applierTooltip)); else if (!applier && statusData.receiverTooltip) tooltipAttributes.push(translateWithContext("status effect", statusData.receiverTooltip)); if (applier) return sprintf(translate("%(statusName)s: %(statusInfo)s %(stackability)s"), { "statusName": headerFont(translateWithContext("status effect", statusData.statusName)), "statusInfo": tooltipAttributes.join(commaFont(translate(", "))), "stackability": getStatusEffectStackabilityTooltip(template) }); return sprintf(translate("%(statusName)s: %(statusInfo)s"), { "statusName": headerFont(translateWithContext("status effect", statusData.statusName)), "statusInfo": tooltipAttributes.join(commaFont(translate(", "))) }); } function getStatusEffectDurationTooltip(template) { if (!template.Duration) return ""; return sprintf(translate("%(durName)s: %(duration)s"), { "durName": headerFont(translate("Duration")), "duration": getSecondsString((template._timeElapsed ? +template.Duration - template._timeElapsed : +template.Duration) / 1000) }); } function getStatusEffectStackabilityTooltip(template) { if (!template.Stackability || template.Stackability == "Ignore") return ""; let stackabilityString = ""; if (template.Stackability === "Extend") stackabilityString = translateWithContext("status effect stackability", "(extends)"); else if (template.Stackability === "Replace") stackabilityString = translateWithContext("status effect stackability", "(replaces)"); else if (template.Stackability === "Stack") stackabilityString = translateWithContext("status effect stackability", "(stacks)"); return sprintf(translate("%(stackability)s"), { "stackability": stackabilityString }); } function getGarrisonTooltip(template) { let tooltips = []; if (template.garrisonHolder) { tooltips.push ( sprintf(translate("%(label)s: %(garrisonLimit)s"), { "label": headerFont(translate("Garrison Limit")), "garrisonLimit": template.garrisonHolder.capacity }) ); if (template.garrisonHolder.buffHeal) tooltips.push( sprintf(translate("%(healRateLabel)s %(value)s %(health)s / %(second)s"), { "healRateLabel": headerFont(translate("Heal:")), "value": Math.round(template.garrisonHolder.buffHeal), "health": unitFont(translateWithContext("garrison tooltip", "Health")), "second": unitFont(translate("second")), }) ); tooltips.join(commaFont(translate(", "))); } if (template.garrisonable) { let extraSize; if (template.garrisonHolder) extraSize = template.garrisonHolder.occupiedSlots; if (template.garrisonable.size > 1 || extraSize) tooltips.push ( sprintf(translate("%(label)s: %(garrisonSize)s %(extraSize)s"), { "label": headerFont(translate("Garrison Size")), "garrisonSize": template.garrisonable.size, "extraSize": extraSize ? translateWithContext("nested garrison", "+ ") + extraSize : "" }) ); } return tooltips.join("\n"); } function getTurretsTooltip(template) { if (!template.turretHolder) return ""; return sprintf(translate("%(label)s: %(turretsLimit)s"), { "label": headerFont(translate("Turret Positions")), "turretsLimit": Object.keys(template.turretHolder.turretPoints).length }); } function getProjectilesTooltip(template) { if (!template.garrisonHolder || !template.buildingAI) return ""; let limit = Math.min( template.buildingAI.maxArrowCount || Infinity, template.buildingAI.defaultArrowCount + Math.round(template.buildingAI.garrisonArrowMultiplier * template.garrisonHolder.capacity) ); if (!limit) return ""; return [ sprintf(translate("%(label)s: %(value)s"), { "label": headerFont(translate("Projectile Limit")), "value": limit }), sprintf(translate("%(label)s: %(value)s"), { "label": headerFont(translateWithContext("projectiles", "Default")), "value": template.buildingAI.defaultArrowCount }), sprintf(translate("%(label)s: %(value)s"), { "label": headerFont(translateWithContext("projectiles", "Per Unit")), "value": +template.buildingAI.garrisonArrowMultiplier.toFixed(2) }) ].join(commaFont(translate(", "))); } function getRepairTimeTooltip(entState) { let result = []; result.push(sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Number of repairers:")), "details": entState.repairable.numBuilders })); if (entState.repairable.numBuilders) { result.push(sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Remaining repair time:")), "details": getSecondsString(Math.floor(entState.repairable.buildTime.timeRemaining)) })); let timeReduction = Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew); result.push(sprintf(translatePlural( "Add another worker to speed up the repairs by %(second)s second.", "Add another worker to speed up the repairs by %(second)s seconds.", timeReduction), { "second": timeReduction })); } else result.push(sprintf(translatePlural( "Add a worker to finish the repairs in %(second)s second.", "Add a worker to finish the repairs in %(second)s seconds.", Math.round(entState.repairable.buildTime.timeRemainingNew)), { "second": Math.round(entState.repairable.buildTime.timeRemainingNew) })); return result.join("\n"); } function getBuildTimeTooltip(entState) { let result = []; result.push(sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Number of builders:")), "details": entState.foundation.numBuilders })); if (entState.foundation.numBuilders) { result.push(sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Remaining build time:")), "details": getSecondsString(Math.floor(entState.foundation.buildTime.timeRemaining)) })); let timeReduction = Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew); result.push(sprintf(translatePlural( "Add another worker to speed up the construction by %(second)s second.", "Add another worker to speed up the construction by %(second)s seconds.", timeReduction), { "second": timeReduction })); } else result.push(sprintf(translatePlural( "Add a worker to finish the construction in %(second)s second.", "Add a worker to finish the construction in %(second)s seconds.", Math.round(entState.foundation.buildTime.timeRemainingNew)), { "second": Math.round(entState.foundation.buildTime.timeRemainingNew) })); return result.join("\n"); } /** * Multiplies the costs for a template by a given batch size. */ function multiplyEntityCosts(template, trainNum) { let totalCosts = {}; for (let r of getCostTypes()) if (template.cost[r]) totalCosts[r] = Math.floor(template.cost[r] * trainNum); return totalCosts; } /** * Helper function for getEntityCostTooltip. */ function getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch = 1, fullBatchSize = 1, remainderBatch = 0) { if (!template.cost) return []; let totalCosts = multiplyEntityCosts(template, buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch); if (template.cost.time) totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", { "entity": entity, "batchSize": buildingsCountToTrainFullBatch > 0 ? fullBatchSize : remainderBatch }) : 1)); let costs = []; for (let type of getCostTypes()) if (totalCosts[type]) costs.push(sprintf(translate("%(component)s %(cost)s"), { "component": resourceIcon(type), "cost": totalCosts[type] })); return costs; } function getGatherTooltip(template) { if (!template.resourceGatherRates) return ""; let rates = {}; for (let resource of g_ResourceData.GetResources()) { let types = [resource.code]; for (let subtype in resource.subtypes) { // We ignore ruins as those are not that common if (subtype == "ruins") continue; let rate = template.resourceGatherRates[resource.code + "." + subtype]; if (rate > 0) rates[resource.code + "_" + subtype] = rate; } } if (!Object.keys(rates).length) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Gather Rates:")), "details": Object.keys(rates).map( type => sprintf(translate("%(resourceIcon)s %(rate)s"), { "resourceIcon": resourceIcon(type), "rate": rates[type].toFixed(2) }) ).join(" ") }); } /** * Returns the resources this entity supplies in the specified entity's tooltip */ function getResourceSupplyTooltip(template) { if (!template.supply) return ""; let supply = template.supply; // Translation: Label in tooltip showing the resource type and quantity of a given resource supply. return sprintf(translate("%(label)s %(component)s %(amount)s"), { "label": headerFont(translate("Resource Supply:")), "component": resourceIcon(supply.type[0]), // Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource. "amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞") }); } /** * @param {Object} template - The entity's template. * @return {string} - The resources this entity rewards to a collecter. */ function getTreasureTooltip(template) { if (!template.treasure) return ""; let resources = {}; for (let resource of g_ResourceData.GetResources()) { let type = resource.code; if (template.treasure.resources[type]) resources[type] = template.treasure.resources[type]; } let resourceNames = Object.keys(resources); if (!resourceNames.length) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Reward:")), "details": resourceNames.map( type => sprintf(translate("%(resourceIcon)s %(reward)s"), { "resourceIcon": resourceIcon(type), "reward": resources[type] }) ).join(" ") }); } function getResourceTrickleTooltip(template) { if (!template.resourceTrickle) return ""; let resCodes = g_ResourceData.GetCodes().filter(res => !!template.resourceTrickle.rates[res]); if (!resCodes.length) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Resource Trickle:")), "details": sprintf(translate("%(resources)s / %(time)s"), { "resources": resCodes.map( res => sprintf(translate("%(resourceIcon)s %(rate)s"), { "resourceIcon": resourceIcon(res), "rate": template.resourceTrickle.rates[res] }) ).join(" "), "time": getSecondsString(template.resourceTrickle.interval / 1000) }) }); } function getUpkeepTooltip(template) { if (!template.upkeep) return ""; let resCodes = g_ResourceData.GetCodes().filter(res => !!template.upkeep.rates[res]); if (!resCodes.length) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Upkeep:")), "details": sprintf(translate("%(resources)s / %(time)s"), { "resources": resCodes.map( res => sprintf(translate("%(resourceIcon)s %(rate)s"), { "resourceIcon": resourceIcon(res), "rate": template.upkeep.rates[res] }) ).join(" "), "time": getSecondsString(template.upkeep.interval / 1000) }) }); } /** * Returns an array of strings for a set of wall pieces. If the pieces share * resource type requirements, output will be of the form '10 to 30 Stone', * otherwise output will be, e.g. '10 Stone, 20 Stone, 30 Stone'. */ function getWallPieceTooltip(wallTypes) { let out = []; let resourceCount = {}; for (let resource of getCostTypes()) if (wallTypes[0].cost[resource]) resourceCount[resource] = [wallTypes[0].cost[resource]]; let sameTypes = true; for (let i = 1; i < wallTypes.length; ++i) { for (let resource in wallTypes[i].cost) // Break out of the same-type mode if this wall requires // resource types that the first didn't. if (wallTypes[i].cost[resource] && !resourceCount[resource]) { sameTypes = false; break; } for (let resource in resourceCount) if (wallTypes[i].cost[resource]) resourceCount[resource].push(wallTypes[i].cost[resource]); else { sameTypes = false; break; } } if (sameTypes) for (let resource in resourceCount) // Translation: This string is part of the resources cost string on // the tooltip for wall structures. out.push(sprintf(translate("%(resourceIcon)s %(minimum)s to %(resourceIcon)s %(maximum)s"), { "resourceIcon": resourceIcon(resource), "minimum": Math.min.apply(Math, resourceCount[resource]), "maximum": Math.max.apply(Math, resourceCount[resource]) })); else for (let i = 0; i < wallTypes.length; ++i) out.push(getEntityCostComponentsTooltipString(wallTypes[i]).join(", ")); return out; } /** * Returns the cost information to display in the specified entity's construction button tooltip. */ function getEntityCostTooltip(template, player, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) { // Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of // their own; the individual wall pieces within it do. if (template.wallSet) { let templateLong = GetTemplateData(template.wallSet.templates.long, player); let templateMedium = GetTemplateData(template.wallSet.templates.medium, player); let templateShort = GetTemplateData(template.wallSet.templates.short, player); let templateTower = GetTemplateData(template.wallSet.templates.tower, player); let wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]); let towerCosts = getEntityCostComponentsTooltipString(templateTower); return sprintf(translate("Walls: %(costs)s"), { "costs": wallCosts.join(" ") }) + "\n" + sprintf(translate("Towers: %(costs)s"), { "costs": towerCosts.join(" ") }); } if (template.cost) { let costs = getEntityCostComponentsTooltipString(template, entity, buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch).join(" "); if (costs) // Translation: Label in tooltip showing cost of a unit, structure or technology. return sprintf(translate("%(label)s %(costs)s"), { "label": headerFont(translate("Cost:")), "costs": costs }); } return ""; } -function getRequiredTechnologyTooltip(technologyEnabled, requiredTechnology, civ) +function getRequirementsTooltip(enabled, requirements, civ) { - if (technologyEnabled) + if (enabled) return ""; - return sprintf(translate("Requires %(technology)s"), { - "technology": getEntityNames(GetTechnologyData(requiredTechnology, civ)) - }); + // Simple requirements (one tech) can be translated on the fly. + if ("Techs" in requirements && !requirements.Techs.includes(" ")) + return sprintf(translate("Requires %(technology)s"), { + "technology": getEntityNames(GetTechnologyData(requirements.Techs, civ)) + }); + return translate(requirements.Tooltip); } /** * Returns the population bonus information to display in the specified entity's construction button tooltip. */ function getPopulationBonusTooltip(template) { if (!template.population || !template.population.bonus) return ""; return sprintf(translate("%(label)s %(bonus)s"), { "label": headerFont(translate("Population Bonus:")), "bonus": template.population.bonus }); } /** * Returns a message with the amount of each resource needed to create an entity. */ function getNeededResourcesTooltip(resources) { if (!resources) return ""; let formatted = []; for (let resource in resources) formatted.push(sprintf(translate("%(component)s %(cost)s"), { "component": '[font="sans-12"]' + resourceIcon(resource) + '[/font]', "cost": Math.ceil(resources[resource]) })); return coloredText( '[font="sans-bold-13"]' + translate("Insufficient resources:") + '[/font]', "red") + " " + formatted.join(" "); } function getSpeedTooltip(template) { if (!template.speed) return ""; const walk = template.speed.walk.toFixed(1); const run = template.speed.run.toFixed(1); if (walk == 0 && run == 0) return ""; const acceleration = template.speed.acceleration.toFixed(1); return sprintf(translate("%(label)s %(speeds)s"), { "label": headerFont(translate("Speed:")), "speeds": sprintf(translate("%(speed)s %(movementType)s"), { "speed": walk, "movementType": unitFont(translate("Walk")) }) + commaFont(translate(", ")) + sprintf(translate("%(speed)s %(movementType)s"), { "speed": run, "movementType": unitFont(translate("Run")) }) + commaFont(translate(", ")) + sprintf(translate("%(speed)s %(movementType)s"), { "speed": acceleration, "movementType": unitFont(translate("Acceleration")) }) }); } function getHealerTooltip(template) { if (!template.heal) return ""; let health = +(template.heal.health.toFixed(1)); let range = +(template.heal.range.toFixed(0)); let interval = +((template.heal.interval / 1000).toFixed(1)); return [ sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", health), { "label": headerFont(translate("Heal:")), "val": health, "unit": unitFont(translatePlural("Health", "Health", health)) }), sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", range), { "label": headerFont(translate("Range:")), "val": range, "unit": unitFont(translatePlural("meter", "meters", range)) }), sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", interval), { "label": headerFont(translate("Interval:")), "val": interval, "unit": unitFont(translatePlural("second", "seconds", interval)) }) ].join(translate(", ")); } function getAurasTooltip(template) { let auras = template.auras || template.wallSet && GetTemplateData(template.wallSet.templates.long).auras; if (!auras) return ""; let tooltips = []; for (let auraID in auras) { let tooltip = sprintf(translate("%(auralabel)s %(aurainfo)s"), { "auralabel": headerFont(sprintf(translate("%(auraname)s:"), { "auraname": getEntityNames(auras[auraID]) })), "aurainfo": bodyFont(translate(auras[auraID].description)) }); let radius = +auras[auraID].radius; if (radius) tooltip += " " + sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", radius), { "label": translateWithContext("aura", "Range:"), "val": radius, "unit": unitFont(translatePlural("meter", "meters", radius)) }); tooltips.push(tooltip); } return tooltips.join("\n"); } function getEntityNames(template) { if (!template.name.specific) return template.name.generic; if (template.name.specific == template.name.generic) return template.name.specific; let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic; let secondaryName; if (g_ShowSecondaryNames) secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific; if (secondaryName) return sprintf(translate("%(primaryName)s (%(secondaryName)s)"), { "primaryName": primaryName, "secondaryName": secondaryName }); return sprintf(translate("%(primaryName)s"), { "primaryName": primaryName }); } function getEntityNamesFormatted(template) { if (!template.name.specific) return setStringTags(template.name.generic, g_TooltipTextFormats.namePrimaryBig); let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic; let secondaryName; if (g_ShowSecondaryNames) secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific; if (!secondaryName || primaryName == secondaryName) return sprintf(translate("%(primaryName)s"), { "primaryName": setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) + setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall) }); // Translation: Example: "Epibátēs Athēnaîos [font="sans-bold-16"](Athenian Marine)[/font]" return sprintf(translate("%(primaryName)s (%(secondaryName)s)"), { "primaryName": setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) + setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall), "secondaryName": setStringTags(secondaryName, g_TooltipTextFormats.nameSecondary) }); } function getEntityPrimaryNameFormatted(template) { let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic; if (!primaryName) return setStringTags(g_SpecificNamesPrimary ? template.name.generic : template.name.specific, g_TooltipTextFormats.namePrimaryBig); return setStringTags(primaryName[0], g_TooltipTextFormats.namePrimaryBig) + setStringTags(primaryName.slice(1).toUpperCase(), g_TooltipTextFormats.namePrimarySmall); } function getVisibleEntityClassesFormatted(template) { if (!template.visibleIdentityClasses || !template.visibleIdentityClasses.length) return ""; return headerFont(translate("Classes:")) + ' ' + bodyFont(template.visibleIdentityClasses.map(c => translate(c)).join(translate(", "))); } function getLootTooltip(template) { if (!template.loot && !template.resourceCarrying) return ""; let resourcesCarried = []; if (template.resourceCarrying) resourcesCarried = calculateCarriedResources( template.resourceCarrying, template.trader && template.trader.goods ); let lootLabels = []; for (let type of g_ResourceData.GetCodes().concat(["xp"])) { let loot = (template.loot && template.loot[type] || 0) + (resourcesCarried[type] || 0); if (!loot) continue; // Translation: %(component) will be the icon for the loot type and %(loot) will be the value. lootLabels.push(sprintf(translate("%(component)s %(loot)s"), { "component": resourceIcon(type), "loot": loot })); } if (!lootLabels.length) return ""; return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Loot:")), "details": lootLabels.join(" ") }); } function getResourceDropsiteTooltip(template) { if (!template || !template.resourceDropsite || !template.resourceDropsite.types) return ""; return sprintf(translate("%(label)s %(icons)s"), { "label": headerFont(translate("Dropsite for:")), "icons": template.resourceDropsite.types.map(type => resourceIcon(type)).join(" ") }); } function showTemplateViewerOnRightClickTooltip() { // Translation: Appears in a tooltip to indicate that right-clicking the corresponding GUI element will open the Template Details GUI page. return translate("Right-click to view more information."); } function showTemplateViewerOnClickTooltip() { // Translation: Appears in a tooltip to indicate that clicking the corresponding GUI element will open the Template Details GUI page. return translate("Click to view more information."); } /** * @param {number} number - A number to shorten using SI prefix. */ function abbreviateLargeNumbers(number) { if (number >= 1e6) return Math.floor(number / 1e6) + translateWithContext("One letter abbreviation for million", 'M'); if (number >= 1e5) return Math.floor(number / 1e3) + translateWithContext("One letter abbreviation for thousand", 'k'); if (number >= 1e4) return (number / 1e3).toFixed(1).replace(/\.0$/, '') + translateWithContext("One letter abbreviation for thousand", 'k'); return number; } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 27245) @@ -1,336 +1,336 @@ /** * 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) }; } deriveProduction(template, civCode) { const production = { "techs": [], "units": [] }; if (!template.Researcher && !template.Trainer) return production; 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); } const appendTechnology = (technologyName) => { const technology = this.loadTechnologyTemplate(technologyName, civCode); if (DeriveTechnologyRequirements(technology, civCode)) production.techs.push(technologyName); }; if (template.Researcher?.Technologies?._string) for (let technologyName of template.Researcher.Technologies._string.split(" ")) { if (technologyName.indexOf("{civ}") != -1) { 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 + civCode; if (Engine.TemplateExists(templateName)) return templateName; warn("No template found for civ " + civCode + "."); return this.PlayerPath + this.DefaultCiv; } /** * 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 (template.Identity.Requirements?.Techs) + return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.Requirements?.Techs]; 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/players/"; TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/"; TemplateLoader.prototype.DefaultCiv = "gaia"; /** * 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/gui/reference/common/TemplateParser.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 27245) @@ -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); const parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {}); parsed.name.internal = templateName; parsed.history = template.Identity.History; parsed.production = this.TemplateLoader.deriveProduction(template, civCode); if (template.Builder) parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode); // Set the minimum phase that this entity is available. // For gaia objects, this is meaningless. - if (!parsed.requiredTechnology) + if (!parsed.requirements) parsed.phase = this.phaseList[0]; - else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology)) - parsed.phase = this.getActualPhase(parsed.requiredTechnology); + else if (this.TemplateLoader.isPhaseTech(parsed.requirements.Techs)) + parsed.phase = this.getActualPhase(parsed.requirements.Techs); else - parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode); + parsed.phase = this.getPhaseOfTechnology(parsed.requirements.Techs, 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); const tech = GetTechnologyDataHelper(template, civCode, g_ResourceData, this.modifiers[civCode] || {}); tech.name.internal = technologyName; if (template.pair !== undefined) { tech.pair = template.pair; tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs); } if (this.TemplateLoader.isPhaseTech(technologyName)) { tech.actualPhase = technologyName; if (tech.replaces !== undefined) tech.actualPhase = tech.replaces[0]; this.phases[technologyName] = tech; } else this.techs[civCode][technologyName] = tech; return tech; } /** * @param {string} phaseCode * @param {string} civCode * @return {Object} Sanitized object containing phase data */ getPhase(phaseCode, civCode) { return this.getTechnology(phaseCode, civCode); } /** * Load and parse the relevant player_{civ}.xml template. */ getPlayer(civCode) { if (civCode in this.players) return this.players[civCode]; let template = this.TemplateLoader.loadPlayerTemplate(civCode); let parsed = { "civbonuses": [], "teambonuses": [], }; if (template.Auras) for (let auraTemplateName of template.Auras._string.split(/\s+/)) if (AuraTemplateExists(auraTemplateName)) if (this.getAura(auraTemplateName).affectsTeam) parsed.teambonuses.push(auraTemplateName); else parsed.civbonuses.push(auraTemplateName); this.players[civCode] = parsed; return parsed; } /** * Provided with an array containing basic information about possible * upgrades, such as that generated by globalscript's GetTemplateDataHelper, * this function loads the actual template data of the upgrades, overwrites * certain values within, then passes an array containing the template data * back to caller. */ getActualUpgradeData(upgradesInfo, civCode) { let newUpgrades = []; for (let upgrade of upgradesInfo) { upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode); const data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {}); data.name.internal = upgrade.entity; data.cost = upgrade.cost; data.icon = upgrade.icon || data.icon; data.tooltip = upgrade.tooltip || data.tooltip; - data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology; + data.requirements = upgrade.requirements || data.requirements; - if (!data.requiredTechnology) + if (!data.requirements) data.phase = this.phaseList[0]; - else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology)) - data.phase = this.getActualPhase(data.requiredTechnology); + else if (this.TemplateLoader.isPhaseTech(data.requirements.Techs)) + data.phase = this.getActualPhase(data.requirements.Techs); else - data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode); + data.phase = this.getPhaseOfTechnology(data.requirements.Techs, 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/diplomacy/playercontrols/SpyRequestButton.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/gui/session/diplomacy/playercontrols/SpyRequestButton.js (revision 27245) @@ -1,142 +1,142 @@ /** * This class is concerned with providing the spy request button that once pressed will * attempt to bribe a unit of the selected enemy for temporary vision sharing against resources. */ DiplomacyDialogPlayerControl.prototype.SpyRequestButton = class { constructor(playerID) { this.playerID = playerID; // Players who requested a spy against this playerID. this.spyRequests = new Set(); let id = "[" + (playerID - 1) + "]"; this.diplomacySpyRequest = Engine.GetGUIObjectByName("diplomacySpyRequest" + id); this.diplomacySpyRequestImage = Engine.GetGUIObjectByName("diplomacySpyRequestImage" + id); this.diplomacySpyRequest.onPress = this.onPress.bind(this); } onPress() { Engine.PostNetworkCommand({ "type": "spy-request", "source": g_ViewedPlayer, "player": this.playerID }); this.spyRequests.add(g_ViewedPlayer); this.update(false); } /** * Called from GUIInterface notification. * @param player is the one who requested a spy. * @param notification.target is the player who shall be spied upon. */ onSpyResponse(notification, player, playerInactive) { // Update the state if the response was against the current row (target player) if (notification.target == this.playerID) { this.spyRequests.delete(player); // Update UI if the currently viewed player sent the request if (player == g_ViewedPlayer) this.update(false); } } update(playerInactive) { let template = GetTemplateData(this.TemplateName); let hidden = playerInactive || !template || !!GetSimState().players[g_ViewedPlayer].disabledTemplates[this.TemplateName] || g_Players[this.playerID].isMutualAlly[g_ViewedPlayer] && !GetSimState().players[g_ViewedPlayer].hasSharedLos; this.diplomacySpyRequest.hidden = hidden; if (hidden) return; let tooltip = translate(this.Tooltip); - if (template.requiredTechnology && - !Engine.GuiInterfaceCall("IsTechnologyResearched", { - "tech": template.requiredTechnology, + if (template.requirements && + !Engine.GuiInterfaceCall("AreRequirementsMet", { + "requirements": template.requirements, "player": g_ViewedPlayer })) { - tooltip += "\n" + getRequiredTechnologyTooltip( + tooltip += "\n" + getRequirementsTooltip( false, - template.requiredTechnology, + template.requirements, GetSimState().players[g_ViewedPlayer].civ); this.diplomacySpyRequest.enabled = false; this.diplomacySpyRequest.tooltip = tooltip; this.diplomacySpyRequestImage.sprite = this.SpriteModifierDisabled + this.Sprite; return; } if (template.cost) { let modifiedTemplate = clone(template); for (let res in template.cost) modifiedTemplate.cost[res] = Math.floor(GetSimState().players[this.playerID].spyCostMultiplier * template.cost[res]); tooltip += "\n" + getEntityCostTooltip(modifiedTemplate); let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": modifiedTemplate.cost, "player": g_ViewedPlayer }); if (neededResources) { tooltip += "\n" + getNeededResourcesTooltip(neededResources); this.diplomacySpyRequest.enabled = false; this.diplomacySpyRequest.tooltip = tooltip; this.diplomacySpyRequestImage.sprite = resourcesToAlphaMask(neededResources) + ":" + this.Sprite; return; } let costRatio = Engine.GetTemplate(this.TemplateName).VisionSharing.FailureCostRatio; if (costRatio) { for (let res in modifiedTemplate.cost) modifiedTemplate.cost[res] = Math.floor(costRatio * modifiedTemplate.cost[res]); tooltip += "\n" + translate(this.TooltipFailed) + "\n" + getEntityCostTooltip(modifiedTemplate); } } let enabled = !this.spyRequests.has(g_ViewedPlayer); this.diplomacySpyRequest.enabled = enabled && controlsPlayer(g_ViewedPlayer); this.diplomacySpyRequest.tooltip = tooltip; this.diplomacySpyRequestImage.sprite = (enabled ? "" : this.SpriteModifierDisabled) + this.Sprite; } }; DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.TemplateName = "special/spy"; DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.Sprite = "stretched:" + "session/icons/bribes.png"; DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.SpriteModifierDisabled = "color:0 0 0 127:grayscale:"; DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.Tooltip = markForTranslation("Bribe a random unit from this player and share its vision during a limited period."); DiplomacyDialogPlayerControl.prototype.SpyRequestButton.prototype.TooltipFailed = markForTranslation("A failed bribe will cost you:"); Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 27245) @@ -1,1295 +1,1293 @@ /** * 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, + const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", { + "requirements": template.requirements, "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), + getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; - if (!technologyEnabled || limits.canBeAddedCount == 0) + if (!requirementsMet || 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.unitAI || !state.unitAI.formations.length)) 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.unitAI && state.unitAI.formations.includes(formation))); }, "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 : ""; const progressSlider = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]"); if (data.item.ghost) { data.button.enabled = false; progressSlider.sprite="color:0 150 250 50"; const size = progressSlider.size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left; progressSlider.size = size; } 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")); progressSlider.sprite = "queueProgressSlider"; const size = progressSlider.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)); progressSlider.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) { const entState = unitEntStates[0]; if (!entState?.researcher?.technologies) return ret; if (!entState.production) warn("Researcher without ProductionQueue found: " + entState.id + "."); return entState.researcher.technologies.map(tech => ({ "tech": tech, "techCostMultiplier": entState.researcher.techCostMultiplier, "researchFacilityId": entState.id, "isUpgrading": !!entState.upgrade && entState.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.researcher || !state.researcher.technologies) continue; if (!state.production) warn("Researcher without ProductionQueue found: " + state.id + "."); // Remove the techs we already have in ret (with the same name and techCostMultiplier) 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.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.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] !== undefined ? data.item.techCostMultiplier[res] : 1; 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, + const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", { + "requirements": template.requirements, "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), + getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; - if (!technologyEnabled || limits.canBeAddedCount == 0) + if (!requirementsMet || 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, + const requirementsMet = !data.item.requirements || + Engine.GuiInterfaceCall("AreRequirementsMet", { + "requirements": data.item.requirements, "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), + getRequirementsTooltip(requirementsMet, data.item.requirements, 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 && + if (!requirementsMet || 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/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 27245) @@ -1,1029 +1,1013 @@ 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); + "requirements": function() { + return this.get("Identity/Requirements"); }, - // 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; + "available": function(gameState) { + const requirements = this.requirements(); + return !requirements || Sim.RequirementsHelper.AreRequirementsMet(requirements, PlayerID); }, "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("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) { 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) { 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(researcher) { let time = +this.get("Cost/BuildTime"); 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); }, "queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid) }, "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.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount(); }, "resourceSupplyNumGatherers": function() { return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers(); }, "isFull": function() { let numGatherers = this.resourceSupplyNumGatherers(); if (numGatherers) return this.maxGatherers() === numGatherers; return undefined; }, "resourceCarrying": function() { return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus(); }, "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, 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, "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/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 27245) @@ -1,2406 +1,2406 @@ /** * Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: * -defining RESS needs * -BO decisions. * > training workers * > building stuff (though we'll send that to bases) * -picking strategy (specific manager?) * -diplomacy -> diplomacyManager * -planning attacks -> attackManager * -picking new CC locations. */ PETRA.HQ = function(Config) { this.Config = Config; this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i // Cache various quantities. this.turnCache = {}; this.lastFailedGather = {}; this.firstBaseConfig = false; // Workers configuration. this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime. this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase). this.towerLapseTime = this.Config.Military.towerLapseTime; this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase). this.fortressLapseTime = this.Config.Military.fortressLapseTime; this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); this.basesManager = new PETRA.BasesManager(this.Config); this.attackManager = new PETRA.AttackManager(this.Config); this.buildManager = new PETRA.BuildManager(); this.defenseManager = new PETRA.DefenseManager(this.Config); this.tradeManager = new PETRA.TradeManager(this.Config); this.navalManager = new PETRA.NavalManager(this.Config); this.researchManager = new PETRA.ResearchManager(this.Config); this.diplomacyManager = new PETRA.DiplomacyManager(this.Config); this.garrisonManager = new PETRA.GarrisonManager(this.Config); this.victoryManager = new PETRA.VictoryManager(this.Config); this.emergencyManager = new PETRA.EmergencyManager(this.Config); this.capturableTargets = new Map(); this.capturableTargetsTime = 0; }; /** More initialisation for stuff that needs the gameState */ PETRA.HQ.prototype.init = function(gameState, queues) { this.territoryMap = PETRA.createTerritoryMap(gameState); // create borderMap: flag cells on the border of the map // then this map will be completed with our frontier in updateTerritories this.borderMap = PETRA.createBorderMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; this.treasures = gameState.getEntities().filter(ent => ent.isTreasure()); this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); this.emergencyManager.init(gameState); }; /** * initialization needed after deserialization (only called when deserialization) */ PETRA.HQ.prototype.postinit = function(gameState) { this.basesManager.postinit(gameState); this.updateTerritories(gameState); }; /** * returns the sea index linking regions 1 and region 2 (supposed to be different land region) * otherwise return undefined * for the moment, only the case land-sea-land is supported */ PETRA.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2) { let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water") return path[1]; if (this.Config.debug > 1) { API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); } return undefined; }; PETRA.HQ.prototype.checkEvents = function(gameState, events) { this.buildManager.checkEvents(gameState, events); if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); for (let evt of events.DiplomacyChanged) { if (evt.player != PlayerID && evt.otherPlayer != PlayerID) continue; // Reset the entities collections which depend on diplomacy gameState.resetOnDiplomacyChanged(); break; } this.basesManager.checkEvents(gameState, events); for (let evt of events.ConstructionFinished) { if (evt.newentity == evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID) continue; if (ent.hasClass("Market") && this.maxFields) this.maxFields = false; } for (let evt of events.OwnershipChanged) // capture events { if (evt.to != PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; if (!ent.hasClass("Unit")) { if (ent.decaying()) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } continue; } ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.hasClass("Trader")) { ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_TRADER); ent.setMetadata(PlayerID, "route", undefined); } if (ent.hasClass("Worker")) { ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER); ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE); } if (ent.hasClass("Ship")) PETRA.setSeaAccess(gameState, ent); if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) ent.setMetadata(PlayerID, "plan", -1); } // deal with the different rally points of training units: the rally point is set when the training starts // for the time being, only autogarrison is used for (let evt of events.TrainingStarted) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) continue; let metadata = ent._entity.trainingQueue[0].metadata; if (metadata && metadata.garrisonType) ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison else ent.unsetRallyPoint(); } for (let evt of events.TrainingFinished) { for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent.position()) { // we are autogarrisoned, check that the holder is registered in the garrisonManager let holder = gameState.getEntityById(ent.garrisonHolderID()); if (holder) this.garrisonManager.registerHolder(gameState, holder); } else if (ent.getMetadata(PlayerID, "garrisonType")) { // we were supposed to be autogarrisoned, but this has failed (may-be full) ent.setMetadata(PlayerID, "garrisonType", undefined); } // Check if this unit is no more needed in its attack plan // (happen when the training ends after the attack is started or aborted) let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = this.attackManager.getPlan(plan); if (!attack || attack.state !== PETRA.AttackPlan.STATE_UNEXECUTED) ent.setMetadata(PlayerID, "plan", -1); } } } for (let evt of events.TerritoryDecayChanged) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) continue; if (evt.to) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } else if (ent.isGarrisonHolder()) this.garrisonManager.removeDecayingStructure(evt.entity); } // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) if (this.Config.difficulty < PETRA.DIFFICULTY_EASY) return; for (let entId of this.decayingStructures) { let ent = gameState.getEntityById(entId); if (ent && ent.decaying() && ent.isOwn(PlayerID)) { let capture = ent.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio < 0.50) continue; let decayToGaia = true; for (let i = 1; i < capture.length; ++i) { if (gameState.isPlayerAlly(i) || !capture[i]) continue; decayToGaia = false; break; } if (decayToGaia) continue; let ratioMax = 0.7 + randFloat(0, 0.1); for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.85 + randFloat(0, 0.1); break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; PETRA.HQ.prototype.handleNewBase = function(gameState) { if (!this.firstBaseConfig) // This is our first base, let us configure our starting resources. this.configFirstBase(gameState); else { // Let us hope this new base will fix our possible resource shortage. this.saveResources = undefined; this.saveSpace = undefined; this.maxFields = false; } }; /** Ensure that all requirements are met when phasing up*/ PETRA.HQ.prototype.checkPhaseRequirements = function(gameState, queues) { if (gameState.getNumberOfPhases() == this.currentPhase) return; let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); let plan; let queue; for (let entityReq of requirements) { // Village requirements are met elsewhere by constructing more houses if (entityReq.class == "Village" || entityReq.class == "NotField") continue; if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) continue; switch (entityReq.class) { case "Town": if (!queues.economicBuilding.hasQueuedUnits() && !queues.militaryBuilding.hasQueuedUnits()) { if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/market")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/temple")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/temple", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Forge", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/forge")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge", { "phaseUp": true }); queue = "militaryBuilding"; break; } } break; default: // All classes not dealt with inside vanilla game. // We put them for the time being on the economic queue, except if wonder queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; if (!queues[queue].hasQueuedUnits()) { let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); if (structure && this.canBuild(gameState, structure)) plan = new PETRA.ConstructionPlan(gameState, structure, { "phaseUp": true }); } } if (plan) { if (queue == "wonder") { gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true }); plan.queueToReset = "majorTech"; } else { gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true }); plan.queueToReset = queue; } queues[queue].addPlan(plan); return; } } }; /** Called by any "phase" research plan once it's started */ PETRA.HQ.prototype.OnPhaseUp = function(gameState, phase) { }; /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ PETRA.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // default template let requirementsDef = [ ["costsResource", 1, "food"] ]; const classesDef = ["Support+Worker"]; let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); // counting the workers that aren't part of a plan let numberOfWorkers = 0; // all workers let numberOfSupports = 0; // only support workers (i.e. non fighting) gameState.getOwnUnits().forEach(ent => { if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_WORKER && ent.getMetadata(PlayerID, "plan") === undefined) { ++numberOfWorkers; if (ent.hasClass("Support")) ++numberOfSupports; } }); let numberInTraining = 0; gameState.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) { numberInTraining += item.count; if (item.metadata && item.metadata.role && item.metadata.role === PETRA.Worker.ROLE_WORKER && item.metadata.plan === undefined) { numberOfWorkers += item.count; if (item.metadata.support) numberOfSupports += item.count; } } }); // Anticipate the optimal batch size when this queue will start // and adapt the batch size of the first and second queued workers to the present population // to ease a possible recovery if our population was drastically reduced by an attack // (need to go up to second queued as it is accounted in queueManager) let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); if (queues.villager.plans[0]) { queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); if (queues.villager.plans[1]) queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); } if (queues.citizenSoldier.plans[0]) { queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); if (queues.citizenSoldier.plans[1]) queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); } let numberOfQueuedSupports = queues.villager.countQueuedUnits(); let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; let numberTotal = numberOfWorkers + numberQueued; if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) return; if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) return; if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) return; // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers // and gives a ratio alfa at startup. let supportRatio = this.supportRatio; let alpha = 0.85; if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field"))) supportRatio = Math.min(this.supportRatio, 0.1); if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks[PETRA.AttackPlan.TYPE_RUSH].length) alpha = 0.7; if (gameState.isCeasefireActive()) alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); let template; if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ]; else requirements = [ ["strength", 1] ]; const classes = [["CitizenSoldier", "Infantry"]]; // We want at least 33% ranged and 33% melee. classes[0].push(pickRandom(["Ranged", "Melee", "Infantry"])); template = this.findBestTrainableUnit(gameState, classes, requirements); } // If the template variable is empty, the default unit (Support unit) will be used // base "0" means automatic choice of base if (!template && templateDef) queues.villager.addPlan(new PETRA.TrainingPlan(gameState, templateDef, { "role": PETRA.Worker.ROLE_WORKER, "base": 0, "support": true }, size, size)); else if (template) queues.citizenSoldier.addPlan(new PETRA.TrainingPlan(gameState, template, { "role": PETRA.Worker.ROLE_WORKER, "base": 0 }, size, size)); }; /** picks the best template based on parameters and classes */ PETRA.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) { let units; if (classes.indexOf("Hero") != -1) units = gameState.findTrainableUnits(classes, []); // We do not want siege tower as AI does not know how to use it nor hero when not explicitely specified. else units = gameState.findTrainableUnits(classes, ["Hero", "SiegeTower"]); if (!units.length) return undefined; let parameters = requirements.slice(); let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources for (let type in remainingResources) { if (availableResources[type] > 800) continue; if (remainingResources[type] > 800) continue; let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; let toAdd = true; for (let param of parameters) { if (param[0] != "costsResource" || param[2] != type) continue; param[1] = Math.min(param[1], costsResource); toAdd = false; break; } if (toAdd) parameters.push(["costsResource", costsResource, type]); } units.sort((a, b) => { let aCost = 1 + a[1].costSum(); let bCost = 1 + b[1].costSum(); let aValue = 0.1; let bValue = 0.1; for (let param of parameters) { if (param[0] == "strength") { aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; } else if (param[0] == "siegeStrength") { aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; } else if (param[0] == "speed") { aValue += a[1].walkSpeed() * param[1]; bValue += b[1].walkSpeed() * param[1]; } else if (param[0] == "costsResource") { // requires a third parameter which is the resource if (a[1].cost()[param[2]]) aValue *= param[1]; if (b[1].cost()[param[2]]) bValue *= param[1]; } else if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aValue *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bValue *= param[1]; } else API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); } return -aValue/aCost + bValue/bCost; }); return units[0][0]; }; /** * returns an entity collection of workers through BaseManager.pickBuilders * TODO: when same accessIndex, sort by distance */ PETRA.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) { return this.basesManager.bulkPickWorkers(gameState, baseRef, number); }; PETRA.HQ.prototype.getTotalResourceLevel = function(gameState, resources, proximity) { return this.basesManager.getTotalResourceLevel(gameState, resources, proximity); }; /** * Returns the current gather rate * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. */ PETRA.HQ.prototype.GetCurrentGatherRates = function(gameState) { return this.basesManager.GetCurrentGatherRates(gameState); }; /** * Returns the wanted gather rate. */ PETRA.HQ.prototype.GetWantedGatherRates = function(gameState) { if (!this.turnCache.wantedRates) this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); return this.turnCache.wantedRates; }; /** * Pick the resource which most needs another worker * How this works: * We get the rates we would want to have to be able to deal with our plans * We get our current rates * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ PETRA.HQ.prototype.pickMostNeededResources = function(gameState, allowedResources = []) { let wantedRates = this.GetWantedGatherRates(gameState); let currentRates = this.GetCurrentGatherRates(gameState); if (!allowedResources.length) allowedResources = Resources.GetCodes(); let needed = []; for (let res of allowedResources) needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] }); needed.sort((a, b) => { if (a.current < a.wanted && b.current < b.wanted) { if (a.current && b.current) return b.wanted / b.current - a.wanted / a.current; if (a.current) return 1; if (b.current) return -1; return b.wanted - a.wanted; } if (a.current < a.wanted || a.wanted && !b.wanted) return -1; if (b.current < b.wanted || b.wanted && !a.wanted) return 1; return a.current - a.wanted - b.current + b.wanted; }); return needed; }; /** * Returns the best position to build a new Civil Center * Whose primary function would be to reach new resources of type "resource". */ PETRA.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) { // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then look for a good spot. Engine.ProfileStart("findEconomicCCLocation"); // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); const dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClasses(["CivCentre", "Unit"]))); let ccList = []; for (let cc of ccEnts.values()) ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); let dpList = []; for (let dp of dpEnts.values()) dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) }); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let scale = 250 * 250; let proxyAccess; let nbShips = this.navalManager.transportShips.length; if (proximity) // this is our first base { // if our first base, ensure room around radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); // scale is the typical scale at which we want to find a location for our first base // look for bigger scale if we start from a ship (access < 2) or from a small island let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) scale = 400 * 400; } let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; let nearbyRejected = Math.square(120); // Reject if too near from any cc let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc let maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc let cut = 60; if (fromStrategic || proximity) // be less restrictive cut = 30; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // With enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // We require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; if (proxyAccess && nbShips == 0 && proxyAccess != index) continue; let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // Checking distance to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // We will be more tolerant for cc around our oversea docks let oversea = false; if (proximity) // This is our first cc, let's do it near our units norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale; else { let minDist = Math.min(); let accessible = false; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < nearbyRejected) { norm = 0; break; } if (!cc.ally) continue; if (dist < nearbyAllyRejected) { norm = 0; break; } if (dist < nearbyAllyDisfavored) norm *= 0.5; if (dist < minDist) minDist = dist; accessible = accessible || index == PETRA.getLandAccess(gameState, cc.ent); } if (norm == 0) continue; if (accessible && minDist > maxAccessRejected) continue; if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc { if (!accessible) { if (minDist > maxNoAccessDisfavored) norm *= 0.5; else norm *= 0.8; } else norm *= 0.5; } // Not near any of our dropsite, except for oversea docks oversea = !accessible && dpList.some(dp => PETRA.getLandAccess(gameState, dp.ent) == index); if (!oversea) { for (let dp of dpList) { let dist = API3.SquareVectorDistance(dp.pos, pos); if (dist < 3600) { norm = 0; break; } else if (dist < 6400) norm *= 0.5; } } if (norm == 0) continue; } if (this.borderMap.map[j] & PETRA.fullBorder_Mask) // disfavor the borders of the map norm *= 0.5; let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j]; for (let res in gameState.sharedScript.resourceMaps) if (res != "food") val += gameState.sharedScript.ccResourceMaps[res].map[j]; val *= norm; // If oversea, be just above threshold to be accepted if nothing else if (oversea) val = Math.max(val, cut + 0.1); if (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); if (bestVal === undefined) return false; if (this.Config.debug > 1) API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); // not good enough. if (bestVal < cut) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new Civil Center * Whose primary function would be to assure territorial continuity with our allies */ PETRA.HQ.prototype.findStrategicCCLocation = function(gameState, template) { // This builds a map. The procedure is fairly simple. // We minimize the Sum((dist - 300)^2) where the sum is on the three nearest allied CC // with the constraints that all CC have dist > 200 and at least one have dist < 400 // This needs at least 2 CC. Otherwise, go back to economic CC. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let ccList = []; let numAllyCC = 0; for (let cc of ccEnts.values()) { let ally = gameState.isPlayerAlly(cc.owner()); ccList.push({ "pos": cc.position(), "ally": ally }); if (ally) ++numAllyCC; } if (numAllyCC < 2) return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); Engine.ProfileStart("findStrategicCCLocation"); // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let currentVal, delta; let distcc0, distcc1, distcc2; let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; // checking distances to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; let minDist = Math.min(); distcc0 = undefined; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { minDist = 0; break; } if (!cc.ally) continue; if (dist < 62000) // Reject if quite near from ally cc { minDist = 0; break; } if (dist < minDist) minDist = dist; if (!distcc0 || dist < distcc0) { distcc2 = distcc1; distcc1 = distcc0; distcc0 = dist; } else if (!distcc1 || dist < distcc1) { distcc2 = distcc1; distcc1 = dist; } else if (!distcc2 || dist < distcc2) distcc2 = dist; } if (minDist < 1 || minDist > 170000 && !this.navalMap) continue; delta = Math.sqrt(distcc0) - favoredDistance; currentVal = delta*delta; delta = Math.sqrt(distcc1) - favoredDistance; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - favoredDistance; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] & PETRA.fullBorder_Mask) currentVal += 10000; if (bestVal !== undefined && currentVal > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = currentVal; bestIdx = i; } if (this.Config.debug > 1) API3.warn("We've found a strategic base with bestVal = " + bestVal); Engine.ProfileStop(); if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new market: if the allies already have a market, build it as far as possible * from it, although not in our border to be able to defend it easily. If no allied market, our second market will * follow the same logic. * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance * for performance reasons. */ PETRA.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (!markets.length) markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures()).toEntityArray(); if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; // No need for more than one market when we cannot trade. if (!Resources.GetTradableCodes().length) return false; // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let bestDistSq; let bestGainMult; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); const isNavalMarket = template.hasClasses(["Naval+Trade"]); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let j = 0; j < this.territoryMap.length; ++j) { // do not try on the narrow border of our territory if (this.borderMap.map[j] & PETRA.narrowFrontier_Mask) continue; if (this.baseAtIndex(j) == 0) // only in our territory continue; // with enough room around to build the market let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other markets let maxVal = 0; let maxDistSq; let maxGainMult; let gainMultiplier; for (let market of markets) { if (isNavalMarket && template.hasClasses(["Naval+Trade"])) { if (PETRA.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) continue; gainMultiplier = traderTemplatesGains.navalGainMultiplier; } else if (PETRA.getLandAccess(gameState, market) == index && !PETRA.isLineInsideEnemyTerritory(gameState, market.position(), pos)) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let distSq = API3.SquareVectorDistance(market.position(), pos); if (gainMultiplier * distSq > maxVal) { maxVal = gainMultiplier * distSq; maxDistSq = distSq; maxGainMult = gainMultiplier; } } if (maxVal == 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; bestDistSq = maxDistSq; bestGainMult = maxGainMult; bestIdx = i; bestJdx = j; } if (this.Config.debug > 1) API3.warn("We found a market position with bestVal = " + bestVal); if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); if (this.Config.debug > 1) API3.warn("this would give a trading gain of " + expectedGain); // Do not keep it if gain is too small, except if this is our first Market. let idx; if (expectedGain < this.tradeManager.minimalGain) { if (template.hasClass("Market") && !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) idx = -1; // Needed by queueplanBuilding manager to keep that Market. else return false; } else idx = this.baseAtIndex(bestJdx); let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, idx, expectedGain]; }; /** * Returns the best position to build defensive buildings (fortress and towers) * Whose primary function is to defend our borders */ PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template) { // We take the point in our territory which is the nearest to any enemy cc // but requiring a minimal distance with our other defensive structures // and not in range of any enemy defensive structure to avoid building under fire. const ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Fortress", "Tower"])).toEntityArray(); let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals { enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) return undefined; } enemyStructures = enemyStructures.toEntityArray(); let wonderMode = gameState.getVictoryConditions().has("wonder"); let wonderDistmin; let wonders; if (wonderMode) { wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); wonderMode = wonders.length != 0; if (wonderMode) wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); } // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let isTower = template.hasClass("Tower"); let isFortress = template.hasClass("Fortress"); let radius; if (isFortress) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); for (let j = 0; j < this.territoryMap.length; ++j) { if (!wonderMode) { // do not try if well inside or outside territory if (!(this.borderMap.map[j] & PETRA.fullFrontier_Mask)) continue; if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower) continue; } if (this.baseAtIndex(j) == 0) // inaccessible cell continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other structures let minDist = Math.min(); let dista = 0; if (wonderMode) { dista = API3.SquareVectorDistance(wonders[0].position(), pos); if (dista < wonderDistmin) continue; dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder } for (let str of enemyStructures) { if (str.foundationProgress() !== undefined) continue; let strPos = str.position(); if (!strPos) continue; let dist = API3.SquareVectorDistance(strPos, pos); if (dist < 6400) // TODO check on true attack range instead of this 80×80 { minDist = -1; break; } if (str.hasClass("CivCentre") && dist + dista < minDist) minDist = dist + dista; } if (minDist < 0) continue; let cutDist = 900; // 30×30 TODO maybe increase it for (let str of ownStructures) { let strPos = str.position(); if (!strPos) continue; if (API3.SquareVectorDistance(strPos, pos) < cutDist) { minDist = -1; break; } } if (minDist < 0 || minDist == Math.min()) continue; if (bestVal !== undefined && minDist > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = minDist; bestIdx = i; bestJdx = j; } if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.baseAtIndex(bestJdx)]; }; PETRA.HQ.prototype.buildTemple = function(gameState, queues) { // at least one market (which have the same queue) should be build before any temple if (queues.economicBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) return; // Try to build a temple earlier if in regicide to recruit healer guards if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide")) return; let templateName = "structures/{civ}/temple"; if (this.canBuild(gameState, "structures/{civ}/temple_vesta")) templateName = "structures/{civ}/temple_vesta"; else if (!this.canBuild(gameState, templateName)) return; queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, templateName)); }; PETRA.HQ.prototype.buildMarket = function(gameState, queues) { if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}/market")) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("Market")) { if (!queues.economicBuilding.paused) { // Put available resources in this market let queueManager = gameState.ai.queueManager; let cost = queues.economicBuilding.plans[0].getCost(); queueManager.setAccounts(gameState, cost, "economicBuilding"); if (!queueManager.canAfford("economicBuilding", cost)) { for (let q in queueManager.queues) { if (q == "economicBuilding") continue; queueManager.transferAccounts(cost, q, "economicBuilding"); if (queueManager.canAfford("economicBuilding", cost)) break; } } } return; } gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market"); plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; /** Build a farmstead */ PETRA.HQ.prototype.buildFarmstead = function(gameState, queues) { // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) return; // Wait to have at least one dropsite and house before the farmstead if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) return; if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) return; if (!this.canBuild(gameState, "structures/{civ}/farmstead")) return; queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/farmstead")); }; /** * Try to build a wonder when required * force = true when called from the victoryManager in case of Wonder victory condition. */ PETRA.HQ.prototype.buildWonder = function(gameState, queues, force = false) { if (queues.wonder && queues.wonder.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}/wonder")) return; if (!force) { let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/wonder")); // Check that we have enough resources to start thinking to build a wonder let cost = template.cost(); let resources = gameState.getResources(); let highLevel = 0; let lowLevel = 0; for (let res in cost) { if (resources[res] && resources[res] > 0.7 * cost[res]) ++highLevel; else if (!resources[res] || resources[res] < 0.3 * cost[res]) ++lowLevel; } if (highLevel == 0 || lowLevel > 1) return; } queues.wonder.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/wonder")); }; /** Build a corral, and train animals there */ PETRA.HQ.prototype.manageCorral = function(gameState, queues) { if (queues.corral.hasQueuedUnits()) return; let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")) && nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral) { if (this.canBuild(gameState, "structures/{civ}/corral")) { queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral")); return; } if (!nCorral) return; } // And train some animals let civ = gameState.getPlayerCiv(); for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) { if (corral.foundationProgress() !== undefined) continue; let trainables = corral.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.isHuntable()) continue; let count = gameState.countEntitiesByType(trainable, true); for (let item of corral.trainingQueue()) count += item.count; if (count > nCorral) continue; queues.corral.addPlan(new PETRA.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); return; } } }; /** * build more houses if needed. * kinda ugly, lots of special cases to both build enough houses but not tooo many… */ PETRA.HQ.prototype.buildMoreHouses = function(gameState, queues) { let houseTemplateString = "structures/{civ}/apartment"; if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) || !this.canBuild(gameState, houseTemplateString)) { houseTemplateString = "structures/{civ}/house"; if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString))) return; } if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return; let numPlanned = queues.house.length(); if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) { let plan = new PETRA.ConstructionPlan(gameState, houseTemplateString); // change the starting condition according to the situation. plan.goRequirement = "houseNeeded"; queues.house.addPlan(plan); } if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) { let houseTemplateName = gameState.applyCiv(houseTemplateString); let houseTemplate = gameState.getTemplate(houseTemplateName); let needed = 0; for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) { if (!houseTemplate.hasClass(entityReq.class)) continue; let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.buildManager.setBuildable(houseTemplateName); this.requireHouses = true; } needed = Math.max(needed, entityReq.count - count); } let houseQueue = queues.house.plans; for (let i = 0; i < numPlanned; ++i) if (houseQueue[i].isGo(gameState)) --needed; else if (needed > 0) { houseQueue[i].goRequirement = undefined; --needed; } } if (this.requireHouses) { let houseTemplate = gameState.getTemplate(gameState.applyCiv(houseTemplateString)); if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) this.requireHouses = undefined; } // When population limit too tight // - if no room to build, try to improve with technology // - otherwise increase temporarily the priority of houses let house = gameState.applyCiv(houseTemplateString); let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; let popBonus = gameState.getTemplate(house).getPopulationBonus(); let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); let priority; if (freeSlots < 5) { if (this.buildManager.isUnbuildable(gameState, house)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to improve with technology"); this.researchManager.researchPopulationBonus(gameState, queues); } else priority = 2 * this.Config.priorities.house; } else priority = this.Config.priorities.house; if (priority && priority != gameState.ai.queueManager.getPriority("house")) gameState.ai.queueManager.changePriority("house", priority); }; /** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.hasQueuedUnits()) return; // First build one cc if all have been destroyed if (!this.hasPotentialBase()) { this.buildFirstBase(gameState); return; } // Then expand if we have not enough room available for buildings if (this.buildManager.numberMissingRoom(gameState) > 1) { if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build "); this.buildNewBase(gameState, queues); return; } // If we've already planned to phase up, wait a bit before trying to expand if (this.phasing) return; // Finally expand if we have lots of units (threshold depending on the aggressivity value) let activeBases = this.numActiveBases(); let numUnits = gameState.getOwnUnits().length; let numvar = 10 * (1 - this.Config.personality.aggressive); if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) { if (this.Config.debug > 2) API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); this.buildNewBase(gameState, queues); } }; PETRA.HQ.prototype.buildNewBase = function(gameState, queues, resource) { if (this.hasPotentialBase() && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) return false; let template; // We require at least one of this civ civCentre as they may allow specific units or techs let hasOwnCC = false; for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) { if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}/civil_centre")) continue; hasOwnCC = true; break; } if (hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) template = "structures/{civ}/military_colony"; else if (this.canBuild(gameState, "structures/{civ}/civil_centre")) template = "structures/{civ}/civil_centre"; else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) template = "structures/{civ}/military_colony"; else return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; /** Deals with building fortresses and towers along our border with enemies. */ PETRA.HQ.prototype.buildDefenses = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) return; if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) { // Try to build fortresses. if (this.canBuild(gameState, "structures/{civ}/fortress")) { let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) && numFortresses < this.numActiveBases() + 1 + this.extraFortresses && numFortresses < Math.floor(gameState.getPopulation() / 25) && gameState.getOwnFoundationsByClass("Fortress").length < 2) { this.fortressStartTime = gameState.ai.elapsedTime; if (!numFortresses) gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/fortress"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); return; } } } if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}/sentry_tower")) { // Count all towers + wall towers. let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length; let towerLapseTime = this.saveResource ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime; if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) { this.fortStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/sentry_tower")); } return; } if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}/defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) && numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && numTowers < Math.floor(gameState.getPopulation() / 8) && gameState.getOwnFoundationsByClass("Tower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; if (numTowers > 2 * this.numActiveBases() + 3) gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding)); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/defense_tower"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); } }; PETRA.HQ.prototype.buildForge = function(gameState, queues) { if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge || queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length) return; // Build a Market before the Forge. if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities()) return; if (this.canBuild(gameState, "structures/{civ}/forge")) queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge")); }; /** * Deals with constructing military buildings (e.g. barracks, stable). * They are mostly defined by Config.js. This is unreliable since changes could be done easily. */ PETRA.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) return; let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; if (this.saveResources && numBarracks != 0) return; let barracksTemplate = this.canBuild(gameState, "structures/{civ}/barracks") ? "structures/{civ}/barracks" : undefined; let rangeTemplate = this.canBuild(gameState, "structures/{civ}/range") ? "structures/{civ}/range" : undefined; let numRanges = gameState.getOwnEntitiesByClass("Range", true).length; let stableTemplate = this.canBuild(gameState, "structures/{civ}/stable") ? "structures/{civ}/stable" : undefined; let numStables = gameState.getOwnEntitiesByClass("Stable", true).length; if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) { // First barracks/range and stable. if (numBarracks + numRanges == 0) { let template = barracksTemplate || rangeTemplate; if (template) { gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); let plan = new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }); plan.queueToReset = "militaryBuilding"; queues.militaryBuilding.addPlan(plan); return; } } if (numStables == 0 && stableTemplate) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Second barracks/range and stable. if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); if (template) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Third barracks/range and stable, if needed. if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) { let template = barracksTemplate || stableTemplate || rangeTemplate; if (template) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } } if (this.saveResources) return; if (this.currentPhase < 3) return; if (this.canBuild(gameState, "structures/{civ}/elephant_stable") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities()) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/elephant_stable", { "militaryBase": true })); return; } if (this.canBuild(gameState, "structures/{civ}/arsenal") && !gameState.getOwnEntitiesByClass("Arsenal", true).hasEntities()) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/arsenal", { "militaryBase": true })); return; } if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) return; // Build advanced military buildings let nAdvanced = 0; for (let advanced of this.bAdvanced) nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) { for (let advanced of this.bAdvanced) { if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) continue; let template = gameState.getTemplate(advanced); if (!template) continue; let civ = gameState.getPlayerCiv(); if (template.hasDefensiveFire() || template.trainableEntities(civ)) queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced, { "militaryBase": true })); else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced)); return; } } }; /** * Find base nearest to ennemies for military buildings. */ PETRA.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase; let enemyFound = false; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) continue; let access = PETRA.getLandAccess(gameState, cce); let isEnemy = gameState.isPlayerEnemy(cce.owner()); for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; if (PETRA.getLandAccess(gameState, cc) != access) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); if (!enemyFound && isEnemy) enemyFound = true; else if (dist > distMin) continue; bestBase = cc.getMetadata(PlayerID, "base"); distMin = dist; } } return bestBase; }; /** * train with highest priority ranged infantry in the nearest civil center from a given set of positions * and garrison them there for defense */ PETRA.HQ.prototype.trainEmergencyUnits = function(gameState, positions) { if (gameState.ai.queues.emergency.hasQueuedUnits()) return false; let civ = gameState.getPlayerCiv(); // find nearest base anchor let distcut = 20000; let nearestAnchor; let distmin; for (let pos of positions) { let access = gameState.ai.accessibility.getAccessValue(pos); // check nearest base anchor for (const base of this.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; if (PETRA.getLandAccess(gameState, base.anchor) != access) continue; if (!base.anchor.trainableEntities(civ)) // base still in construction continue; let queue = base.anchor._entity.trainingQueue; if (queue) { let time = 0; for (let item of queue) if (item.progress > 0 || item.metadata && item.metadata.garrisonType) time += item.timeRemaining; if (time/1000 > 5) continue; } let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (nearestAnchor && dist > distmin) continue; distmin = dist; nearestAnchor = base.anchor; } } if (!nearestAnchor || distmin > distcut) return false; // We will choose randomly ranged and melee units, except when garrisonHolder is full // in which case we prefer melee units let numGarrisoned = this.garrisonManager.numberOfGarrisonedSlots(nearestAnchor); if (nearestAnchor._entity.trainingQueue) { for (let item of nearestAnchor._entity.trainingQueue) { if (item.metadata && item.metadata.garrisonType) numGarrisoned += item.count; else if (!item.progress && (!item.metadata || !item.metadata.trainer)) nearestAnchor.stopProduction(item.id); } } let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); let rangedWanted = randBool() && autogarrison; let total = gameState.getResources(); let templateFound; let trainables = nearestAnchor.trainableEntities(civ); let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.hasClasses(["Infantry+CitizenSoldier"])) continue; if (autogarrison && !template.hasClasses(garrisonArrowClasses)) continue; if (!total.canAfford(new API3.Resources(template.cost()))) continue; templateFound = [trainable, template]; if (template.hasClass("Ranged") == rangedWanted) break; } if (!templateFound) return false; // Check first if we can afford it without touching the other accounts // and if not, take some of other accounted resources // TODO sort the queues to be substracted let queueManager = gameState.ai.queueManager; let cost = new API3.Resources(templateFound[1].cost()); queueManager.setAccounts(gameState, cost, "emergency"); if (!queueManager.canAfford("emergency", cost)) { for (let q in queueManager.queues) { if (q == "emergency") continue; queueManager.transferAccounts(cost, q, "emergency"); if (queueManager.canAfford("emergency", cost)) break; } } const metadata = { "role": PETRA.Worker.ROLE_WORKER, "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; if (autogarrison) metadata.garrisonType = PETRA.GarrisonManager.TYPE_PROTECTION; gameState.ai.queues.emergency.addPlan(new PETRA.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); return true; }; PETRA.HQ.prototype.canBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.buildManager.isUnbuildable(gameState, type)) return false; if (gameState.isTemplateDisabled(type)) { this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); return false; } let template = gameState.getTemplate(type); if (!template) { this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); return false; } if (!template.available(gameState)) { - this.buildManager.setUnbuildable(gameState, type, 30, "tech"); + this.buildManager.setUnbuildable(gameState, type, 30, "requirements"); return false; } if (!this.buildManager.hasBuilder(type)) { this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); return false; } if (!this.hasActiveBase()) { // if no base, check that we can build outside our territory let buildTerritories = template.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) { this.buildManager.setUnbuildable(gameState, type, 180, "room"); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) { this.buildManager.setUnbuildable(gameState, type, 90, "limit"); return false; } return true; }; PETRA.HQ.prototype.updateTerritories = function(gameState) { const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; let alliedVictory = gameState.getAlliedVictory(); let passabilityMap = gameState.getPassabilityMap(); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let insideSmall = Math.round(45 / cellSize); let insideLarge = Math.round(80 / cellSize); // should be about the range of towers let expansion = 0; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.borderMap.map[j] & PETRA.outside_Mask) continue; if (this.borderMap.map[j] & PETRA.fullFrontier_Mask) this.borderMap.map[j] &= ~PETRA.fullFrontier_Mask; // reset the frontier if (this.territoryMap.getOwnerIndex(j) != PlayerID) this.basesManager.removeBaseFromTerritoryIndex(j); else { // Update the frontier let ix = j%width; let iz = Math.floor(j/width); let onFrontier = false; for (let a of around) { let jx = ix + Math.round(insideSmall*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(insideSmall*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) { this.borderMap.map[j] |= PETRA.narrowFrontier_Mask; break; } jx = ix + Math.round(insideLarge*a[0]); if (jx < 0 || jx >= width) continue; jz = iz + Math.round(insideLarge*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) onFrontier = true; } if (onFrontier && !(this.borderMap.map[j] & PETRA.narrowFrontier_Mask)) this.borderMap.map[j] |= PETRA.largeFrontier_Mask; if (this.basesManager.addTerritoryIndexToBase(gameState, j, passabilityMap)) expansion++; } } if (!expansion) return; // We've increased our territory, so we may have some new room to build this.buildManager.resetMissingRoom(gameState); // And if sufficient expansion, check if building a new market would improve our present trade routes let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; if (expansion * cellArea > 960) this.tradeManager.routeProspection = true; }; /** * returns the base corresponding to baseID */ PETRA.HQ.prototype.getBaseByID = function(baseID) { return this.basesManager.getBaseByID(baseID); }; /** * returns the number of bases with a cc * ActiveBases includes only those with a built cc * PotentialBases includes also those with a cc in construction */ PETRA.HQ.prototype.numActiveBases = function() { return this.basesManager.numActiveBases(); }; PETRA.HQ.prototype.hasActiveBase = function() { return this.basesManager.hasActiveBase(); }; PETRA.HQ.prototype.numPotentialBases = function() { return this.basesManager.numPotentialBases(); }; PETRA.HQ.prototype.hasPotentialBase = function() { return this.basesManager.hasPotentialBase(); }; PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) { return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); }; /** Check that the chosen position is not too near from an invading army */ PETRA.HQ.prototype.isNearInvadingArmy = function(pos) { for (let army of this.defenseManager.armies) if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) return true; return false; }; PETRA.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) { if (!this.turnCache.firingStructures) this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); for (let ent of this.turnCache.firingStructures.values()) { let range = radius + ent.attackRange("Ranged").max; if (API3.SquareVectorDistance(ent.position(), pos) < range*range) return true; } return false; }; /** Compute the capture strength of all units attacking a capturable target */ PETRA.HQ.prototype.updateCaptureStrength = function(gameState) { this.capturableTargets.clear(); for (let ent of gameState.getOwnUnits().values()) { if (!ent.canCapture()) continue; let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || !target.isCapturable() || !ent.canCapture(target)) continue; if (!this.capturableTargets.has(targetId)) this.capturableTargets.set(targetId, { "strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"), "ents": new Set([ent.id()]) }); else { let capturableTarget = this.capturableTargets.get(target.id()); capturableTarget.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturableTarget.ents.add(ent.id()); } } for (let [targetId, capturableTarget] of this.capturableTargets) { let target = gameState.getEntityById(targetId); let allowCapture; for (let entId of capturableTarget.ents) { let ent = gameState.getEntityById(entId); if (allowCapture === undefined) allowCapture = PETRA.allowCapture(gameState, ent, target); let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].attackType) continue; if ((orderData[0].attackType == "Capture") !== allowCapture) ent.attack(targetId, allowCapture); } } this.capturableTargetsTime = gameState.ai.elapsedTime; }; /** * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) */ PETRA.HQ.prototype.isDefendable = function(ent) { if (!this.turnCache.numAround) this.turnCache.numAround = {}; if (this.turnCache.numAround[ent.id()] === undefined) this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); return +this.turnCache.numAround[ent.id()] > 8; }; /** * Get the number of population already accounted for */ PETRA.HQ.prototype.getAccountedPopulation = function(gameState) { if (this.turnCache.accountedPopulation == undefined) { let pop = gameState.getPopulation(); for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.unitTemplate) continue; let unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population"); if (unitPop) pop += item.count * unitPop; } } this.turnCache.accountedPopulation = pop; } return this.turnCache.accountedPopulation; }; /** * Get the number of workers already accounted for */ PETRA.HQ.prototype.getAccountedWorkers = function(gameState) { if (this.turnCache.accountedWorkers == undefined) { let workers = gameState.getOwnEntitiesByRole(PETRA.Worker.ROLE_WORKER, true).length; for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.metadata || !item.metadata.role || item.metadata.role !== PETRA.Worker.ROLE_WORKER) continue; workers += item.count; } } this.turnCache.accountedWorkers = workers; } return this.turnCache.accountedWorkers; }; PETRA.HQ.prototype.baseManagers = function() { return this.basesManager.baseManagers; }; /** * @param {number} territoryIndex - The index to get the map for. * @return {number} - The ID of the base at the given territory index. */ PETRA.HQ.prototype.baseAtIndex = function(territoryIndex) { return this.basesManager.baseAtIndex(territoryIndex); }; /** * Some functions are run every turn * Others once in a while */ PETRA.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.emergencyManager.update(gameState); this.turnCache = {}; this.territoryMap = PETRA.createTerritoryMap(gameState); this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(API3.Filters.isBuilt()).hasEntities(); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) { if (this.Config.debug > 0) API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + " phasing " + this.phasing); this.currentPhase = gameState.currentPhase(); // In principle, this.phasing should be already reset to 0 when starting the research // but this does not work in case of an autoResearch tech if (this.phasing) this.phasing = 0; } /* if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; PETRA.dumpEntity(ent); }); } */ this.checkEvents(gameState, events); this.navalManager.checkEvents(gameState, queues, events); if (this.phasing) this.checkPhaseRequirements(gameState, queues); else this.researchManager.checkPhase(gameState, queues); if (this.hasActiveBase()) { if (gameState.ai.playedTurn % 4 == 0) this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 4 == 1) this.buildMoreHouses(gameState, queues); if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) this.buildFarmstead(gameState, queues); if (this.needCorral && gameState.ai.playedTurn % 4 == 3) this.manageCorral(gameState, queues); if (gameState.ai.playedTurn % 5 == 1) this.researchManager.update(gameState, queues); } if (!this.hasPotentialBase() || this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) this.checkBaseExpansion(gameState, queues); if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) { if (!this.canBarter) this.buildMarket(gameState, queues); if (!this.saveResources) { this.buildForge(gameState, queues); this.buildTemple(gameState, queues); } if (gameState.ai.playedTurn % 30 == 0 && gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) this.buildWonder(gameState, queues, false); } this.tradeManager.update(gameState, events, queues); this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); if (gameState.ai.playedTurn % 3 == 0) { this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > PETRA.DIFFICULTY_SANDBOX) this.buildDefenses(gameState, queues); } this.basesManager.update(gameState, queues, events); this.navalManager.update(gameState, queues, events); if (this.Config.difficulty > PETRA.DIFFICULTY_SANDBOX && (this.hasActiveBase() || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); this.victoryManager.update(gameState, events, queues); // We update the capture strength at the end as it can change attack orders if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3) this.updateCaptureStrength(gameState); Engine.ProfileStop(); }; PETRA.HQ.prototype.Serialize = function() { let properties = { "phasing": this.phasing, "lastFailedGather": this.lastFailedGather, "firstBaseConfig": this.firstBaseConfig, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "fortressStartTime": this.fortressStartTime, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "needCorral": this.needCorral, "needFarm": this.needFarm, "needFish": this.needFish, "maxFields": this.maxFields, "canExpand": this.canExpand, "canBuildUnits": this.canBuildUnits, "navalMap": this.navalMap, "landRegions": this.landRegions, "navalRegions": this.navalRegions, "decayingStructures": this.decayingStructures, "capturableTargets": this.capturableTargets, "capturableTargetsTime": this.capturableTargetsTime }; if (this.Config.debug == -100) { API3.warn(" HQ serialization ---------------------"); API3.warn(" properties " + uneval(properties)); API3.warn(" basesManager " + uneval(this.basesManager.Serialize())); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); API3.warn(" emergencyManager " + uneval(this.emergencyManager.Serialize())); } return { "properties": properties, "basesManager": this.basesManager.Serialize(), "attackManager": this.attackManager.Serialize(), "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), "tradeManager": this.tradeManager.Serialize(), "navalManager": this.navalManager.Serialize(), "researchManager": this.researchManager.Serialize(), "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "victoryManager": this.victoryManager.Serialize(), "emergencyManager": this.emergencyManager.Serialize(), }; }; PETRA.HQ.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.basesManager = new PETRA.BasesManager(this.Config); this.basesManager.init(gameState); this.basesManager.Deserialize(gameState, data.basesManager); this.navalManager = new PETRA.NavalManager(this.Config); this.navalManager.init(gameState, true); this.navalManager.Deserialize(gameState, data.navalManager); this.attackManager = new PETRA.AttackManager(this.Config); this.attackManager.Deserialize(gameState, data.attackManager); this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); this.buildManager = new PETRA.BuildManager(); this.buildManager.Deserialize(data.buildManager); this.defenseManager = new PETRA.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); this.tradeManager = new PETRA.TradeManager(this.Config); this.tradeManager.init(gameState); this.tradeManager.Deserialize(gameState, data.tradeManager); this.researchManager = new PETRA.ResearchManager(this.Config); this.researchManager.Deserialize(data.researchManager); this.diplomacyManager = new PETRA.DiplomacyManager(this.Config); this.diplomacyManager.Deserialize(data.diplomacyManager); this.garrisonManager = new PETRA.GarrisonManager(this.Config); this.garrisonManager.Deserialize(data.garrisonManager); this.victoryManager = new PETRA.VictoryManager(this.Config); this.victoryManager.Deserialize(data.victoryManager); this.emergencyManager = new PETRA.EmergencyManager(this.Config); this.emergencyManager.Deserialize(data.emergencyManager); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 27245) @@ -1,945 +1,945 @@ /** * Defines a construction plan, ie a building. * We'll try to fing a good position if non has been provided */ PETRA.ConstructionPlan = function(gameState, type, metadata, position) { if (!PETRA.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; PETRA.ConstructionPlan.prototype = Object.create(PETRA.QueuePlan.prototype); PETRA.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn return false; if (!this.isGo(gameState)) return false; - if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) + if (!this.template.available(gameState)) return false; return gameState.ai.HQ.buildManager.hasBuilder(this.type); }; PETRA.ConstructionPlan.prototype.start = function(gameState) { Engine.ProfileStart("Building construction start"); // We don't care which builder we assign, since they won't actually do // the building themselves - all we care about is that there is at least // one unit that can start the foundation (should always be the case here). let builder = gameState.findBuilder(this.type); if (!builder) { API3.warn("petra error: builder not found when starting construction."); Engine.ProfileStop(); return; } let pos = this.findGoodPosition(gameState); if (!pos) { gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); Engine.ProfileStop(); return; } if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") || gameState.getOwnEntitiesByClass("Market", true).hasEntities())) { // Check if this Market is still worth building (others may have been built making it useless). let tradeManager = gameState.ai.HQ.tradeManager; tradeManager.checkRoutes(gameState); if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { Engine.ProfileStop(); return; } } gameState.ai.HQ.turnCache.buildingBuilt = true; if (this.metadata === undefined) this.metadata = { "base": pos.base }; else if (this.metadata.base === undefined) this.metadata.base = pos.base; if (pos.access) this.metadata.access = pos.access; // needed for Docks whose position is on water else this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); if (this.template.buildPlacementType() == "shore") { // adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); else // try with the lowest, move towards us unless we're same { for (let step = 0; step <= 1; step += 0.2) builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, pos.angle, this.metadata); } this.onStart(gameState); Engine.ProfileStop(); if (this.metadata && this.metadata.proximity) gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); }; PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState) { let template = this.template; if (template.buildPlacementType() == "shore") return this.findDockPosition(gameState); let HQ = gameState.ai.HQ; if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; const newpos = base.findBestDropsiteLocation(gameState, type, template._templateName); if (newpos && newpos.quality > 0) { let pos = newpos.pos; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; } } if (!this.position) { if (template.hasClass("CivCentre")) { let pos; if (this.metadata && this.metadata.resource) { let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; // No possible location, try to build instead a dock in a not-enemy island let templateName = gameState.applyCiv("structures/{civ}/dock"); if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) { template = gameState.getTemplate(templateName); if (template && gameState.getResources().canAfford(new API3.Resources(template.cost()))) this.buildOverseaDock(gameState, template); } return false; } else if (template.hasClasses(["Tower", "Fortress", "ArmyCamp"])) { let pos = HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; // if this fortress is our first one, just try the standard placement if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) return false; } else if (template.hasClass("Market")) // Docks are done before. { let pos = HQ.findMarketLocation(gameState, template); if (pos && pos[2] > 0) { if (!this.metadata) this.metadata = {}; this.metadata.expectedGain = pos[3]; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; } else if (!pos) return false; } } // Compute each tile's closeness to friendly structures: let placement = new API3.Map(gameState.sharedScript, "territory"); let cellSize = placement.cellSize; // size of each tile let alreadyHasHouses = false; if (this.position) // If a position was specified then place the building as close to it as possible { let x = Math.floor(this.position[0] / cellSize); let z = Math.floor(this.position[1] / cellSize); placement.addInfluence(x, z, 255); } else // No position was specified so try and find a sensible place to build { // give a small > 0 level as the result of addInfluence is constrained to be > 0 // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) if (HQ.baseAtIndex(j) == base) placement.set(j, 45); } else { for (let j = 0; j < placement.map.length; ++j) if (HQ.baseAtIndex(j) != 0) placement.set(j, 45); } if (!HQ.requireHouses || !template.hasClass("House")) { gameState.getOwnStructures().forEach(function(ent) { let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); let struct = PETRA.getBuiltEntity(gameState, ent); if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) { if (template.hasClasses(["Field", "Corral"])) placement.addInfluence(x, z, 80 / cellSize, 50); else // If this is not a field add a negative influence because we want to leave this area for fields placement.addInfluence(x, z, 80 / cellSize, -20); } else if (template.hasClass("House")) { if (ent.hasClass("House")) { placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses alreadyHasHouses = true; } else if (ent.hasClasses(["Gate", "!Wall"])) placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs } else if (template.hasClass("Farmstead") && !ent.hasClasses(["Field", "Corral"]) && ent.hasClasses(["Gate", "!Wall"])) placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) placement.addInfluence(x, z, 120 / cellSize, -50); else if (template.hasClass("Military")) placement.addInfluence(x, z, 40 / cellSize, -40); else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) placement.addInfluence(x, z, 60 / cellSize, 40); }); } if (template.hasClass("Farmstead")) { for (let j = 0; j < placement.map.length; ++j) { let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; if (HQ.borderMap.map[j] & PETRA.fullBorder_Mask) value /= 2; // we need space around farmstead, so disfavor map border placement.set(j, value); } } } // Requires to be inside our territory, and inside our base territory if required // and if our first market, put it on border if possible to maximize distance with next Market. let favorBorder = template.hasClass("Market"); let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); let favoredBase = this.metadata && (this.metadata.favoredBase || (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (HQ.baseAtIndex(j) != base) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask) placement.set(j, placement.map[j] + 50); else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask)) placement.set(j, placement.map[j] + 10); let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (HQ.baseAtIndex(j) == 0) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask) placement.set(j, placement.map[j] + 50); else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask)) placement.set(j, placement.map[j] + 10); let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; else if (favoredBase && HQ.baseAtIndex(j) == favoredBase) placement.set(j, placement.map[j] + 100); } } } // Find the best non-obstructed: // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, // this allows room for units to walk between buildings. // note: not for houses and dropsites who ought to be closer to either each other or a resource. // also not for fields who can be stacked quite a bit let obstructions = PETRA.createObstructionMap(gameState, 0, template); // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let radius = 0; if (template.hasClasses(["Fortress", "Arsenal"]) || this.type == gameState.applyCiv("structures/{civ}/elephant_stable")) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClasses(["House", "Field", "Market"])) radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let bestTile; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); if (!bestTile.val) bestTile = undefined; } if (!bestTile) bestTile = placement.findBestTile(radius, obstructions); if (!bestTile.val) return false; let bestIdx = bestTile.idx; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; let territorypos = placement.gamePosToMapPos([x, z]); let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.baseAtIndex(territoryIndex) }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory * => we try to minimize distance from our current point * metadata.oversea is defined for dock in oversea islands * => we try to maximize distance to our current docks (for trade) * otherwise standard dock on an island where we already have a cc * => we try not to be too far from our territory * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. */ PETRA.ConstructionPlan.prototype.findDockPosition = function(gameState) { let template = this.template; let territoryMap = gameState.ai.HQ.territoryMap; let obstructions = PETRA.createObstructionMap(gameState, 0, template); // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let bestIdx; let bestJdx; let bestAngle; let bestLand; let bestWater; let bestVal = -1; let navalPassMap = gameState.ai.accessibility.navalPassMap; let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let nbShips = gameState.ai.HQ.navalManager.transportShips.length; let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; if (nbShips == 0 && proxyAccess && proxyAccess > 1) { wantedLand = {}; wantedLand[proxyAccess] = true; } let dropsiteTypes = template.resourceDropsiteTypes(); let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let halfSize = 0; // used for dock angle let halfDepth = 0; // used by checkPlacement let halfWidth = 0; // used by checkPlacement if (template.get("Footprint/Square")) { halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; halfDepth = +template.get("Footprint/Square/@depth") / 2; halfWidth = +template.get("Footprint/Square/@width") / 2; } else if (template.get("Footprint/Circle")) { halfSize = +template.get("Footprint/Circle/@radius"); halfDepth = halfSize; halfWidth = halfSize; } // res is a measure of the amount of resources around, and maxRes is the max value taken into account // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement const maxRes = 10; const maxWater = 16; let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null; let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null; // Normalisation factors (only guessed, no attempt to optimize them) let factor = proxyAccess ? 1 : oversea ? 0.2 : 40; for (let j = 0; j < territoryMap.length; ++j) { if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) continue; let score = 0; if (!proxyAccess && !oversea) { // if not in our (or allied) territory, we do not want it too far to be able to defend it score = this.getFrontierProximity(gameState, j); if (score > 4) continue; score *= factor; } let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; if (wantedSea && navalPassMap[i] != wantedSea) continue; let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // If proximity is given, we look for the nearest point if (proxyAccess) score = API3.VectorDistance(this.metadata.proximity, pos); // Bonus for resources score += 20 * (maxRes - res); if (oversea) { // Not much farther to one of our cc than to enemy ones let enemyDist; let ownDist; for (let cc of ccEnts.values()) { let owner = cc.owner(); if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) continue; let dist = API3.SquareVectorDistance(pos, cc.position()); if (owner == PlayerID && (!ownDist || dist < ownDist)) ownDist = dist; if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) enemyDist = dist; } if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) continue; // And maximize distance for trade. let dockDist = 0; for (let dock of docks.values()) { if (PETRA.getSeaAccess(gameState, dock) != navalPassMap[i]) continue; let dist = API3.SquareVectorDistance(pos, dock.position()); if (dist > dockDist) dockDist = dist; } if (dockDist > 0) { dockDist = Math.sqrt(dockDist); if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away continue; score += factor * (width * cellSize - dockDist); } } // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] & PETRA.fullBorder_Mask) score += 20; // Do a pre-selection, supposing we will have the best possible water if (bestIdx !== undefined && score > bestVal + 5 * maxWater) continue; let x = (i % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; let angle = this.getDockAngle(gameState, x, z, halfSize); if (angle == false) continue; let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) continue; // Final selection now that the checkDockPlacement water is known if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) continue; if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = score + maxWater - ret.water; bestIdx = i; bestJdx = j; bestAngle = angle; bestLand = ret.land; bestWater = ret.water; } if (bestVal < 0) return false; // if no good place with enough water around and still in first phase, wait for expansion at the next phase if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Assign this dock to a base let baseIndex = gameState.ai.HQ.baseAtIndex(bestJdx); if (!baseIndex) baseIndex = -2; // We'll do an anchorless base around it return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; /** * Find a good island to build a dock. */ PETRA.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template) { let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); if (!docks.hasEntities()) return; let passabilityMap = gameState.getPassabilityMap(); let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let land = {}; let found; for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { if (gameState.ai.accessibility.regionType[i] != "land" || cellArea * gameState.ai.accessibility.regionSize[i] < 3600) continue; let keep = true; for (let dock of docks.values()) { if (PETRA.getLandAccess(gameState, dock) != i) continue; keep = false; break; } if (!keep) continue; let sea; for (let cc of ccEnts.values()) { let ccAccess = PETRA.getLandAccess(gameState, cc); if (ccAccess != i) { if (cc.owner() == PlayerID && !sea) sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); continue; } // Docks on island where we have a cc are already done elsewhere if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) { keep = false; break; } } if (!keep || !sea) continue; land[i] = true; found = true; } if (!found) return; if (!gameState.ai.HQ.navalMap) API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there "); let oldTemplate = this.template; let oldMetadata = this.metadata; this.template = template; let pos; this.metadata = { "land": land, "oversea": true }; pos = this.findDockPosition(gameState); if (pos) { let type = template.templateName(); let builder = gameState.findBuilder(type); this.metadata.base = pos.base; // Adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } this.template = oldTemplate; this.metadata = oldMetadata; }; /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ PETRA.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let seaRef = gameState.ai.accessibility.navalPassMap[k]; if (seaRef < 2) return false; const numPoints = 16; for (let dist = 0; dist < 4; ++dist) { let waterPoints = []; for (let i = 0; i < numPoints; ++i) { let angle = 2 * Math.PI * i / numPoints; pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; pos = gameState.ai.accessibility.gamePosToMapPos(pos); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.navalPassMap[j] == seaRef) waterPoints.push(i); } let length = waterPoints.length; if (!length) continue; let consec = []; for (let i = 0; i < length; ++i) { let 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; } let start = 0; let count = 0; for (let 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 false; }; /** * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js * to determine the special dock requirements * returns {"land": land index for this dock, "water": amount of water around this spot} */ PETRA.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) { let sz = halfDepth * Math.sin(angle); let cz = halfDepth * Math.cos(angle); // center back position let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); let j = pos[0] + pos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[j]; if (land < 2) return null; // center front position pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) return null; // additional constraints compared to BuildRestriction.js to assure we have enough place to build let sw = halfWidth * Math.cos(angle) * 3 / 4; let cw = halfWidth * Math.sin(angle) * 3 / 4; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; let water = 0; let sp = 15 * Math.sin(angle); let cp = 15 * Math.cos(angle); for (let i = 1; i < 5; ++i) { pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; water += 4; } return { "land": land, "water": water }; }; /** * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension * if the (object) wantedLand is given, this nearest land should have one of these accessibility * if wantedSea is given, this tile should be inside this sea */ PETRA.ConstructionPlan.prototype.around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], [-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]]; PETRA.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) { let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let dimLand = dimension + 1.5 * cellSize; let dimSea = dimension + 2 * cellSize; let accessibility = gameState.ai.accessibility; let x = (j%width + 0.5) * cellSize; let z = (Math.floor(j/width) + 0.5) * cellSize; let pos = accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*accessibility.width; let landPass = accessibility.landPassMap[k]; if (landPass > 1 && wantedLand && !wantedLand[landPass] || landPass < 2 && accessibility.navalPassMap[k] < 2) return false; for (let a of this.around) { pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); if (pos[0] < 0 || pos[0] >= accessibility.width) continue; if (pos[1] < 0 || pos[1] >= accessibility.height) continue; k = pos[0] + pos[1]*accessibility.width; landPass = accessibility.landPassMap[k]; if (landPass < 2 || wantedLand && !wantedLand[landPass]) continue; pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); if (pos[0] < 0 || pos[0] >= accessibility.width) continue; if (pos[1] < 0 || pos[1] >= accessibility.height) continue; k = pos[0] + pos[1]*accessibility.width; if (wantedSea && accessibility.navalPassMap[k] != wantedSea || !wantedSea && accessibility.navalPassMap[k] < 2) continue; return true; } return false; }; /** * return a measure of the proximity to our frontier (including our allies) * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m */ PETRA.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) { let alliedVictory = gameState.getAlliedVictory(); let territoryMap = gameState.ai.HQ.territoryMap; let territoryOwner = territoryMap.getOwnerIndex(j); if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) return 0; let borderMap = gameState.ai.HQ.borderMap; let width = territoryMap.width; let step = Math.round(24 / territoryMap.cellSize); let ix = j % width; let iz = Math.floor(j / width); let best = 5; for (let a of this.around) { for (let i = 1; i < 5; ++i) { let jx = ix + Math.round(i*step*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(i*step*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) { best = Math.min(best, i); break; } } if (best == 1) break; } return best; }; /** * get the sum of the resources (except food) around, inside a given radius * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood */ PETRA.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) { let resourceMaps = gameState.sharedScript.resourceMaps; let w = resourceMaps.wood.width; let cellSize = resourceMaps.wood.cellSize; let size = Math.floor(radius / cellSize); let ix = i % w; let iy = Math.floor(i / w); let total = 0; let nbcell = 0; for (let k of types) { if (k == "food" || !resourceMaps[k]) continue; let weigh0 = k == "wood" ? 2 : 1; for (let dy = 0; dy <= size; ++dy) { let dxmax = size - dy; let ky = iy + dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } if (dy == 0) continue; ky = iy - dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } } } return nbcell ? total / nbcell : 0; }; PETRA.ConstructionPlan.prototype.isGo = function(gameState) { if (this.goRequirement && this.goRequirement == "houseNeeded") { if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/house") && !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/apartment")) return false; if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return false; let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); for (let ent of gameState.getOwnFoundations().values()) { let template = gameState.getBuiltTemplate(ent.templateName()); if (template) freeSlots += template.getPopulationBonus(); } if (gameState.ai.HQ.saveResources) return freeSlots <= 10; if (gameState.getPopulation() > 55) return freeSlots <= 21; if (gameState.getPopulation() > 30) return freeSlots <= 15; return freeSlots <= 10; } return true; }; PETRA.ConstructionPlan.prototype.onStart = function(gameState) { if (this.queueToReset) gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); }; PETRA.ConstructionPlan.prototype.Serialize = function() { return { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position, "goRequirement": this.goRequirement || undefined, "queueToReset": this.queueToReset || undefined }; }; PETRA.ConstructionPlan.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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 27245) @@ -1,2140 +1,2134 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronized for the biggest part, // so most of the attributes shouldn't be serialized. // Return an object with a small selection of deterministic data. return { "timeNotifications": this.timeNotifications, "timeNotificationID": this.timeNotificationID }; }; GuiInterface.prototype.Deserialize = function(data) { this.Init(); this.timeNotifications = data.timeNotifications; this.timeNotificationID = data.timeNotificationID; }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; this.templateModified = {}; this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function() { let ret = { "players": [] }; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { const playerEnt = cmpPlayerManager.GetPlayerByID(i); const cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); const cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); const cmpIdentity = Engine.QueryInterface(playerEnt, IID_Identity); // Work out which phase we are in. let phase = ""; const cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } let allies = []; let mutualAllies = []; let neutrals = []; let enemies = []; for (let j = 0; j < numPlayers; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } ret.players.push({ "name": cmpIdentity.GetName(), "civ": cmpIdentity.GetCiv(), "color": cmpPlayer.GetColor(), "controlsAll": cmpPlayer.CanControlAllUnits(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "resourceGatherers": cmpPlayer.GetResourceGatherers(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), "hasSharedLos": cmpPlayer.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": cmpPlayer.CanBarter(), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer) }); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = cmpTerrain.GetMapSize(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function() { let ret = this.GetSimulationState(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; /** * Returns the gamesettings that were chosen at the time the match started. */ GuiInterface.prototype.GetInitAttributes = function() { return InitAttributes; }; /** * This data will be stored in the replay metadata file after a match has been finished recording. */ GuiInterface.prototype.GetReplayMetadata = function() { let extendedSimState = this.GetExtendedSimulationState(); return { "timeElapsed": extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "mapSettings": InitAttributes.settings }; }; /** * Called when the game ends if the current game is part of a campaign run. */ GuiInterface.prototype.GetCampaignGameEndData = function(player) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (Trigger.prototype.OnCampaignGameEnd) return Trigger.prototype.OnCampaignGameEnd(); return {}; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function() { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({ "entity": entity, "newentity": mirage }); }; /** * Get common entity info, often used in the gui. */ GuiInterface.prototype.GetEntityState = function(player, ent) { if (!ent) return null; // All units must have a template; if not then it's a nonexistent entity id. const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(ent); if (!template) return null; const ret = { "id": ent, "player": INVALID_PLAYER, "template": template }; const cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras) ret.auras = cmpAuras.GetDescriptions(); let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) ret.identity = { "rank": cmpIdentity.GetRank(), "rankTechName": cmpIdentity.GetRankTechName(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "controllable": cmpIdentity.IsControllable() }; const cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) ret.formation = { "members": cmpFormation.GetMembers() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) ret.position = cmpPosition.GetPosition(); let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured(); ret.needsHeal = !cmpHealth.IsUnhealable(); } let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; let cmpMarket = QueryMiragedInterface(ent, IID_Market); if (cmpMarket) ret.market = { "land": cmpMarket.HasType("land"), "naval": cmpMarket.HasType("naval") }; let cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress() }; let cmpPopulation = Engine.QueryInterface(ent, IID_Population); if (cmpPopulation) ret.population = { "bonus": cmpPopulation.GetPopBonus() }; let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades": cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo(), "isUpgrading": cmpUpgrade.IsUpgrading() }; const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher); if (cmpResearcher) ret.researcher = { "technologies": cmpResearcher.GetTechnologiesList(), "techCostMultiplier": cmpResearcher.GetTechCostMultiplier() }; let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver); if (cmpStatusEffects) ret.statusEffects = cmpStatusEffects.GetActiveStatuses(); let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "queue": cmpProductionQueue.GetQueue(), "autoqueue": cmpProductionQueue.IsAutoQueueing() }; const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer); if (cmpTrainer) ret.trainer = { "entities": cmpTrainer.GetEntitiesList() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) ret.foundation = { "numBuilders": cmpFoundation.GetNumBuilders(), "buildTime": cmpFoundation.GetBuildTime() }; let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders(), "buildTime": cmpRepairable.GetBuildTime() }; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) ret.player = cmpOwnership.GetOwner(); let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "occupiedSlots": cmpGarrisonHolder.OccupiedSlots() }; let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); if (cmpTurretHolder) ret.turretHolder = { "turretPoints": cmpTurretHolder.GetTurretPoints() }; let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (cmpTurretable) ret.turretable = { "ejectable": cmpTurretable.IsEjectable(), "holder": cmpTurretable.HolderID() }; let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) ret.garrisonable = { "holder": cmpGarrisonable.HolderID(), "size": cmpGarrisonable.UnitSize() }; let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "selectableStances": cmpUnitAI.GetSelectableStances(), "isIdle": cmpUnitAI.IsIdle(), "formations": cmpUnitAI.GetFormationsList(), "formation": cmpUnitAI.GetFormationController() }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) ret.guard = { "entities": cmpGuard.GetEntities() }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } let cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) ret.gate = { "locked": cmpGate.IsLocked() }; let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) ret.alertRaiser = true; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); let cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (let type of types) { ret.attack[type] = {}; Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); ret.attack[type].attackName = cmpAttack.GetAttackName(type); ret.attack[type].splash = cmpAttack.GetSplashData(type); if (ret.attack[type].splash) Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true)); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; ret.attack[type].yOrigin = cmpAttack.GetAttackYOrigin(type); let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } if (cmpPosition && cmpPosition.IsInWorld()) // For units, take the range in front of it, no spread, so angle = 0, // else, take the average elevation around it: angle = 2 * pi. ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, ret.attack[type].yOrigin, cmpUnitAI ? 0 : 2 * Math.PI); else // Not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } let cmpResistance = QueryMiragedInterface(ent, IID_Resistance); if (cmpResistance) ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity"); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "numGatherers": cmpResourceSupply.GetNumGatherers() }; let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes(), "sharable": cmpResourceDropsite.IsSharable(), "shared": cmpResourceDropsite.IsShared() }; let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "health": cmpHeal.GetHealth(), "range": cmpHeal.GetRange().max, "interval": cmpHeal.GetInterval(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses() }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { ret.loot = cmpLoot.GetResources(); ret.loot.xp = cmpLoot.GetXp(); } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) ret.resourceTrickle = { "interval": cmpResourceTrickle.GetInterval(), "rates": cmpResourceTrickle.GetRates() }; let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure); if (cmpTreasure) ret.treasure = { "collectTime": cmpTreasure.CollectionTime(), "resources": cmpTreasure.Resources() }; let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector); if (cmpTreasureCollector) ret.treasureCollector = true; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(), "acceleration": cmpUnitMotion.GetAcceleration() }; let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep); if (cmpUpkeep) ret.upkeep = { "interval": cmpUpkeep.GetInterval(), "rates": cmpUpkeep.GetRates() }; return ret; }; GuiInterface.prototype.GetMultipleEntityStates = function(player, ents) { return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) })); }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); let rot = { "x": 0, "y": 0, "z": 0 }; let pos = { "x": cmd.x, "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z), "z": cmd.z }; const yOrigin = cmd.yOrigin || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, yOrigin, 2 * Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, data) { let templateName = data.templateName; let owner = data.player !== undefined ? data.player : player; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) return GetTemplateDataHelper(template, owner, aurasTemplate, Resources); let auraNames = template.Auras._string.split(/\s+/); for (let name of auraNames) { let auraTemplate = AuraTemplates.Get(name); if (!auraTemplate) error("Template " + templateName + " has undefined aura " + name); else aurasTemplate[name] = auraTemplate; } return GetTemplateDataHelper(template, owner, aurasTemplate, Resources); }; -GuiInterface.prototype.IsTechnologyResearched = function(player, data) +GuiInterface.prototype.AreRequirementsMet = 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); + return !data.requirements || RequirementsHelper.AreRequirementsMet(data.requirements, + data.player !== undefined ? data.player : player); }; /** * Checks whether the requirements for this technology have been met. */ GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) { let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(data.tech); }; /** * Returns technologies that are being actively researched, along with * which entity is researching them and how far along the research is. */ GuiInterface.prototype.GetStartedResearch = function(player) { return QueryPlayerIDInterface(player, IID_TechnologyManager)?.GetBasicInfoOfStartedTechs() || {}; }; /** * Returns the battle state of the player. */ GuiInterface.prototype.GetBattleState = function(player) { let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (!cmpBattleDetection) return false; return cmpBattleDetection.GetState(); }; /** * Returns a list of ongoing attacks against the player. */ GuiInterface.prototype.GetIncomingAttacks = function(player) { let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); if (!cmpAttackDetection) return []; return cmpAttackDetection.GetIncomingAttacks(); }; /** * Used to show a red square over GUI elements you can't yet afford. */ GuiInterface.prototype.GetNeededResources = function(player, data) { let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player); return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {}; }; /** * State of the templateData (player dependent): true when some template values have been modified * and need to be reloaded by the gui. */ GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) { return this.templateModified[player] || false; }; GuiInterface.prototype.ResetTemplateModified = function() { this.templateModified = {}; }; /** * Some changes may require an update to the selection panel, * which is cached for efficiency. Inform the GUI it needs reloading. */ GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.SetSelectionDirty = function(player) { this.selectionDirty[player] = true; }; GuiInterface.prototype.IsSelectionDirty = function(player) { return this.selectionDirty[player] || false; }; GuiInterface.prototype.ResetSelectionDirty = function() { this.selectionDirty = {}; }; /** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) * so they should allways be added and deleted in a deterministic way. */ GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); notification.endTime = duration + cmpTimer.GetTime(); notification.id = ++this.timeNotificationID; // Let all players and observers receive the notification by default. if (!notification.players) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } this.timeNotifications.push(notification); this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime); cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID); }; GuiInterface.prototype.GetTimeNotifications = function(player) { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // Filter on players and time, since the delete timer might be executed with a delay. return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time); }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") this.AddTimeNotification(notification); else this.notifications.push(notification); }; GuiInterface.prototype.GetNotifications = function() { let n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { let cmpPlayer = QueryPlayerIDInterface(wantedPlayer); if (!cmpPlayer) return []; return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return {}; return { "name": template.Identity.GenericName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Identity.Icon }; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { return data.ents.some(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate; }); }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance) return true; } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; for (let ent of cmd.entities) { let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (let building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data) { let updateEntityColor = (iids, entities) => { for (let ent of entities) for (let iid of iids) { let cmp = Engine.QueryInterface(ent, iid); if (cmp) cmp.UpdateColor(); } }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i, IID_Player); if (!cmpPlayer) continue; cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors); if (data.displayDiplomacyColors) cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]); updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ? [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer], cmpRangeManager.GetEntitiesByPlayer(i)); } updateEntityColor([IID_Selectable, IID_StatusBars], data.selected); Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors(); }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { // Cache of owner -> color map let playerColors = {}; for (let ent of cmd.entities) { let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color. let owner = INVALID_PLAYER; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); let color = playerColors[owner]; if (!color) { color = { "r": 1, "g": 1, "b": 1 }; let cmpPlayer = QueryPlayerIDInterface(owner); if (cmpPlayer) color = cmpPlayer.GetDisplayedColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(this.entsWithAuraAndStatusBars); }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { let affectedEnts = new Set(); for (let ent of cmd.entities) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (!cmpStatusBars) continue; cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; for (let name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; for (let e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); else this.entsWithAuraAndStatusBars.delete(ent); } } for (let ent of affectedEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RegenerateSprites(); } }; GuiInterface.prototype.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player); }; GuiInterface.prototype.GetNonGaiaEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them. for (let ent of this.entsRallyPointsDisplayed) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities. for (let ent of cmd.entities) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // Entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location). let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner. let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position. let pos; if (cmd.x && cmd.z) pos = cmd; else // May return undefined if no rally point is set. pos = cmpRallyPoint.GetPositions()[0]; if (pos) { // Only update the position if we changed it (cmd.queued is set). // Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z. if ("queued" in cmd) { if (cmd.queued == true) cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z)); else cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z)); } else if (!cmpRallyPointRenderer.IsSet()) // Rebuild the renderer when not set (when reading saved game or in case of building update). for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z)); cmpRallyPointRenderer.SetDisplayed(true); // Remember which entities have their rally points displayed so we can hide them again. this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(cmd.owner); let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": localisation info (optional) * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { let result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [] }; if (!this.placementEntity || this.placementEntity[0] != cmd.template) { if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; let pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // Set it to a red shade if this is an invalid location. let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { let wallSet = cmd.wallSet; // Did the start position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let start = { "pos": cmd.start, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // Did the end position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let end = { "pos": cmd.end, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // -------------------------------------------------------------------------------- // Do some entity cache management and check for snapping. if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // We're clearing the preview, clear the entity cache and bail. for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // Keep template data around. } return false; } for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) { let pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before. for (let type in wallSet.templates) { if (type == "curves") continue; let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, { "templateName": tpl }), }; if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } // Prevent division by zero errors further on if the start and end positions are the same. if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { // Value of 0.5 was determined through trial and error. let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; let startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { let endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // Clear the single-building preview entity (we'll be rolling our own). this.SetBuildingPlacementPreview(player, { "template": "" }); // -------------------------------------------------------------------------------- // Calculate wall placement and position preview entities. let result = { "pieces": [], "cost": { "population": 0, "time": 0 } }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) // See helpers/Walls.js. previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group. let startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true // Preview only, must not appear in the result. }); } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle }); } if (end.pos) { // Analogous to the starting side case above. if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || []; previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup()); } // If we're snapping to a foundation, add an extra preview tower and also set it to the same control group. let endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true }); } } else previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle }); } let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. let allPiecesValid = true; // Number of entities that are required to build the entire wall, regardless of validity. let numRequiredPieces = 0; for (let i = 0; i < previewEntities.length; ++i) { let entInfo = previewEntities[i]; let ent = null; let tpl = entInfo.template; let tplData = this.placementWallEntities[tpl].templateData; let entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else ent = entPool.entities[entPool.numUsed]; if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // Move piece to right location. // TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities. let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces. if (tpl === wallSet.templates.tower) { let terrainGroundPrev = null; let terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { let targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. let primaryControlGroup = ent; let secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); let validPlacement = false; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region. // TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta. let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden"; if (visible) { let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement. validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success; // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) ++numRequiredPieces; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: We should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. for (let res of Resources.GetCodes().concat(["population", "time"])) result.cost[res] += tplData.cost[res]; } let canAfford = true; let cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) canAfford = false; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } ++entPool.numUsed; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest. // (TODO: Break unlikely ties by choosing the lowest entity ID.) let minDist2 = -1; let minDistEntitySnapData = null; let radius2 = data.snapRadius * data.snapRadius; for (let ent of data.snapEntities) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = { "x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent }; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (data.snapToEdges) { let position = this.obstructionSnap.getPosition(data, template); if (position) return position; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySoundForPlayer = function(player, data) { let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player); let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(data.name); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player); }; GuiInterface.prototype.PlaySound = function(player, data) { if (!data.entity) return; PlaySound(data.name, data.entity); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. let bucket = filtered.bucket; if (bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle()) return { "idle": false }; let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned()) return { "idle": false }; const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable); if (cmpTurretable && cmpTurretable.IsTurreted()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if (!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market); return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; let firstMarket = cmpEntityTrader.GetFirstMarket(); let secondMarket = cmpEntityTrader.GetSecondMarket(); let result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGoods().amount; } else if (data.target === secondMarket) result = { "type": "is second", "gain": cmpEntityTrader.GetGoods().amount, }; else if (!firstMarket) result = { "type": "set first" }; else if (!secondMarket) result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; else result = { "type": "set first" }; return result; }; GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { return Engine.QueryInterface(data.entity, IID_Trainer)?.GetBatchTime(data.batchSize) || 0; }; GuiInterface.prototype.IsMapRevealed = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for (let ent of data.entities) { let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of traders) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { let holder = cmpUnitAI.order.data.target; let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return []; return cmpPlayer.GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; /** * List the GuiInterface functions that can be safely called by GUI scripts. * (GUI scripts are non-deterministic and untrusted, so these functions must be * appropriately careful. They are called with a first argument "player", which is * trusted and indicates the player associated with the current client; no data should * be returned unless this player is meant to be able to see it.) */ let exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetInitAttributes": 1, "GetReplayMetadata": 1, "GetCampaignGameEndData": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetMultipleEntityStates": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, - "IsTechnologyResearched": 1, + "AreRequirementsMet": 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/Identity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 27245) @@ -1,223 +1,221 @@ function Identity() {} Identity.prototype.Schema = "Specifies various names and values associated with the entity, typically for GUI display to users." + "" + "athen" + "Athenian Hoplite" + "Hoplī́tēs Athēnaïkós" + "units/athen_infantry_spearman.png" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "Basic" + "Advanced" + "Elite" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + - "" + - "" + - "" + + RequirementsHelper.BuildSchema() + "" + "" + "" + "" + "" + "" + "" + "" + ""; Identity.prototype.Init = function() { this.classesList = GetIdentityClasses(this.template); this.visibleClassesList = GetVisibleIdentityClasses(this.template); if (this.template.Phenotype) this.phenotype = pickRandom(this.GetPossiblePhenotypes()); else this.phenotype = "default"; this.controllable = this.template.Controllable ? this.template.Controllable == "true" : true; }; Identity.prototype.GetCiv = function() { return this.template.Civ; }; Identity.prototype.GetLang = function() { return this.template.Lang || "greek"; // ugly default }; /** * Get a list of possible Phenotypes. * @return {string[]} A list of possible phenotypes. */ Identity.prototype.GetPossiblePhenotypes = function() { return this.template.Phenotype._string.split(/\s+/); }; /** * Get the current Phenotype. * @return {string} The current phenotype. */ Identity.prototype.GetPhenotype = function() { return this.phenotype; }; Identity.prototype.GetRank = function() { return this.template.Rank || ""; }; Identity.prototype.GetRankTechName = function() { return this.template.Rank ? "unit_" + this.template.Rank.toLowerCase() : ""; }; Identity.prototype.GetClassesList = function() { return this.classesList; }; Identity.prototype.GetVisibleClassesList = function() { return this.visibleClassesList; }; Identity.prototype.HasClass = function(name) { return this.GetClassesList().indexOf(name) != -1; }; Identity.prototype.GetSelectionGroupName = function() { return this.template.SelectionGroupName || ""; }; Identity.prototype.GetGenericName = function() { return this.template.GenericName; }; Identity.prototype.IsUndeletable = function() { return this.template.Undeletable == "true"; }; Identity.prototype.IsControllable = function() { return this.controllable; }; Identity.prototype.SetControllable = function(controllability) { this.controllable = controllability; }; Identity.prototype.SetPhenotype = function(phenotype) { this.phenotype = phenotype; }; /** * @param {string} newName - */ Identity.prototype.SetName = function(newName) { this.name = newName; }; /** * @return {string} - */ Identity.prototype.GetName = function() { return this.name || this.template.GenericName; }; function IdentityMirage() {} IdentityMirage.prototype.Init = function(cmpIdentity) { // Mirages don't get identity classes via the template-filter, so that code can query // identity components via Engine.QueryInterface without having to explicitly check for mirages. // This is cloned as otherwise we get a reference to Identity's property, // and that array is deleted when serializing (as it's not seralized), which ends in OOS. this.classes = clone(cmpIdentity.GetClassesList()); }; IdentityMirage.prototype.GetClassesList = function() { return this.classes; }; Engine.RegisterGlobal("IdentityMirage", IdentityMirage); Identity.prototype.Mirage = function() { let mirage = new IdentityMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_Identity, "Identity", Identity); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 27245) @@ -1,552 +1,552 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; /** * This object represents a technology under research. * @param {string} templateName - The name of the template to research. * @param {number} player - The player ID researching. * @param {number} researcher - The entity ID researching. */ TechnologyManager.prototype.Technology = function(templateName, player, researcher) { this.player = player; this.researcher = researcher; this.templateName = templateName; }; /** * Prepare for the queue. * @param {Object} techCostMultiplier - The multipliers to use when calculating costs. * @return {boolean} - Whether the technology was successfully initiated. */ TechnologyManager.prototype.Technology.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] * template.cost[res]); // ToDo: Subtract resources here or in cmpResearcher? const cmpPlayer = Engine.QueryInterface(this.player, IID_Player); // TrySubtractResources should report error to player (they ran out of resources). if (!cmpPlayer?.TrySubtractResources(this.resources)) return false; const time = techCostMultiplier.time * (template.researchTime || 0) * 1000; this.timeRemaining = time; this.timeTotal = time; const playerID = cmpPlayer.GetPlayerID(); Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).CallEvent("OnResearchQueued", { "playerid": playerID, "technologyTemplate": this.templateName, "researcherEntity": this.researcher }); return true; }; TechnologyManager.prototype.Technology.prototype.Stop = function() { const cmpPlayer = Engine.QueryInterface(this.player, IID_Player); cmpPlayer?.RefundResources(this.resources); delete this.resources; if (this.started && this.templateName.startsWith("phase")) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": this.templateName, "phaseState": "aborted" }); }; /** * Called when the first work is performed. */ TechnologyManager.prototype.Technology.prototype.Start = function() { this.started = true; if (!this.templateName.startsWith("phase")) return; const cmpPlayer = Engine.QueryInterface(this.player, IID_Player); Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": this.templateName, "phaseState": "started" }); }; TechnologyManager.prototype.Technology.prototype.Finish = function() { this.finished = true; const template = TechnologyTemplates.Get(this.templateName); if (template.soundComplete) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher); if (template.modifications) { const cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("tech/" + this.templateName, DeriveModificationsFromTech(template), this.player); } const cmpEntityLimits = Engine.QueryInterface(this.player, IID_EntityLimits); const cmpTechnologyManager = Engine.QueryInterface(this.player, IID_TechnologyManager); if (template.replaces && template.replaces.length > 0) for (const i of template.replaces) { cmpTechnologyManager.MarkTechnologyAsResearched(i); cmpEntityLimits?.UpdateLimitsFromTech(i); } cmpTechnologyManager.MarkTechnologyAsResearched(this.templateName); // ToDo: Move to EntityLimits.js. cmpEntityLimits?.UpdateLimitsFromTech(this.templateName); const playerID = Engine.QueryInterface(this.player, IID_Player).GetPlayerID(); Engine.PostMessage(this.player, MT_ResearchFinished, { "player": playerID, "tech": this.templateName }); if (this.templateName.startsWith("phase") && !template.autoResearch) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "phase", "players": [playerID], "phaseName": this.templateName, "phaseState": "completed" }); }; /** * @param {number} allocatedTime - The time allocated to this item. * @return {number} - The time used for this item. */ TechnologyManager.prototype.Technology.prototype.Progress = function(allocatedTime) { if (!this.started) this.Start(); if (this.paused) this.Unpause(); if (this.timeRemaining > allocatedTime) { this.timeRemaining -= allocatedTime; return allocatedTime; } this.Finish(); return this.timeRemaining; }; TechnologyManager.prototype.Technology.prototype.Pause = function() { this.paused = true; }; TechnologyManager.prototype.Technology.prototype.Unpause = function() { delete this.paused; }; TechnologyManager.prototype.Technology.prototype.GetBasicInfo = function() { return { "paused": this.paused, "progress": 1 - (this.timeRemaining / (this.timeTotal || 1)), "researcher": this.researcher, "templateName": this.templateName, "timeRemaining": this.timeRemaining }; }; TechnologyManager.prototype.Technology.prototype.SerializableAttributes = [ "paused", "player", "researcher", "resources", "started", "templateName", "timeRemaining", "timeTotal" ]; TechnologyManager.prototype.Technology.prototype.Serialize = function() { const result = {}; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; return result; }; TechnologyManager.prototype.Technology.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; }; TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the technology object. this.researchQueued = new Map(); 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.SerializableAttributes = [ "researchedTechs", "classCounts", "typeCountsByClass", "unresearchedAutoResearchTechs" ]; TechnologyManager.prototype.Serialize = function() { const result = {}; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; result.researchQueued = []; for (const [techName, techObject] of this.researchQueued) result.researchQueued.push(techObject.Serialize()); return result; }; TechnologyManager.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; this.researchQueued = new Map(); for (const tech of data.researchQueued) { const newTech = new this.Technology(); newTech.Deserialize(tech); this.researchQueued.set(tech.templateName, newTech); } }; 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 (template.Identity?.Requirements) + return RequirementsHelper.AreRequirementsMet(template.Identity.Requirements, Engine.QueryInterface(this.entity, IID_Player).GetPlayerID()); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.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_Identity).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {Object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) 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]; } } } } }; /** * This does neither apply effects nor verify requirements. * @param {string} tech - The name of the technology to mark as researched. */ TechnologyManager.prototype.MarkTechnologyAsResearched = function(tech) { this.researchedTechs.add(tech); this.UpdateAutoResearch(); }; /** * Note that this does not verify whether the requirements are met. * @param {string} tech - The technology to research. * @param {number} researcher - Optionally the entity to couple with the research. */ TechnologyManager.prototype.ResearchTechnology = function(tech, researcher = INVALID_ENTITY) { if (this.IsTechnologyQueued(tech) || this.IsTechnologyResearched(tech)) return; const technology = new this.Technology(tech, this.entity, researcher); technology.Finish(); }; /** * Marks a technology as being queued for research at the given entityID. * @param {string} tech - The technology to queue. * @param {number} researcher - The entity ID of the entity researching this technology. * @param {Object} techCostMultiplier - The multipliers used when calculating the costs. * * @return {boolean} - Whether we successfully have queued the technology. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher, techCostMultiplier) { // ToDo: Check whether the technology is researched already? const technology = new this.Technology(tech, this.entity, researcher); if (!technology.Queue(techCostMultiplier)) return false; this.researchQueued.set(tech, technology); return true; }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. * @param {string} tech - The name of the technology to stop. * @param {boolean} notification - Whether a GUI notification ought to be sent. */ TechnologyManager.prototype.StoppedResearch = function(tech) { this.researchQueued.get(tech).Stop(); this.researchQueued.delete(tech); }; /** * @param {string} tech - */ TechnologyManager.prototype.Pause = function(tech) { this.researchQueued.get(tech).Pause(); }; /** * @param {string} tech - The technology to advance. * @param {number} allocatedTime - The time allocated to the technology. * @return {number} - The time we've actually used. */ TechnologyManager.prototype.Progress = function(techName, allocatedTime) { const technology = this.researchQueued.get(techName); const usedTime = technology.Progress(allocatedTime); if (technology.finished) this.researchQueued.delete(techName); return usedTime; }; /** * @param {string} tech - The technology name to retreive some basic information for. * @return {Object} - Some basic information about the technology under research. */ TechnologyManager.prototype.GetBasicInfo = function(tech) { return this.researchQueued.get(tech).GetBasicInfo(); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.GetBasicInfoOfStartedTechs = function() { const result = {}; for (const [techName, tech] of this.researchQueued) if (tech.started) result[techName] = tech.GetBasicInfo(); return result; }; /** * 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/Upgrade.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 27245) @@ -1,369 +1,364 @@ function Upgrade() {} const UPGRADING_PROGRESS_INTERVAL = 250; Upgrade.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeInteger") + "" + "" + "" + "" + "" + - "" + - "" + - "" + - "" + - "" + - "" + + RequirementsHelper.BuildSchema() + "" + "" + "" + "" + "" + "" + ""; Upgrade.prototype.Init = function() { this.elapsedTime = 0; this.expendedResources = {}; }; // This will also deal with the "OnDestroy" case. Upgrade.prototype.OnOwnershipChanged = function(msg) { if (!this.completed) this.CancelUpgrade(msg.from); if (msg.to != INVALID_PLAYER) { this.owner = msg.to; this.DetermineUpgrades(); } }; Upgrade.prototype.DetermineUpgrades = function() { this.upgradeTemplates = {}; for (const choice in this.template) { const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity).GetCiv(); const playerCiv = QueryPlayerIDInterface(this.owner, IID_Identity).GetCiv(); const name = this.template[choice].Entity. replace(/\{native\}/g, nativeCiv). replace(/\{civ\}/g, playerCiv); if (!Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(name)) continue; if (this.upgradeTemplates[name]) warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used."); this.upgradeTemplates[name] = choice; } }; Upgrade.prototype.ChangeUpgradedEntityCount = function(amount) { if (!this.IsUpgrading()) return; let cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTempMan.GetTemplate(this.upgrading); let categoryTo; if (template.TrainingRestrictions) categoryTo = template.TrainingRestrictions.Category; else if (template.BuildRestrictions) categoryTo = template.BuildRestrictions.Category; if (!categoryTo) return; let categoryFrom; let cmpTrainingRestrictions = Engine.QueryInterface(this.entity, IID_TrainingRestrictions); let cmpBuildRestrictions = Engine.QueryInterface(this.entity, IID_BuildRestrictions); if (cmpTrainingRestrictions) categoryFrom = cmpTrainingRestrictions.GetCategory(); else if (cmpBuildRestrictions) categoryFrom = cmpBuildRestrictions.GetCategory(); if (categoryTo == categoryFrom) return; let cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits); if (cmpEntityLimits) cmpEntityLimits.ChangeCount(categoryTo, amount); }; Upgrade.prototype.CanUpgradeTo = function(template) { return this.upgradeTemplates[template] !== undefined; }; Upgrade.prototype.GetUpgrades = function() { let ret = []; for (const option in this.upgradeTemplates) { const choice = this.template[this.upgradeTemplates[option]]; let cost = {}; if (choice.Cost) cost = this.GetResourceCosts(option); if (choice.Time) cost.time = this.GetUpgradeTime(option); let hasCost = choice.Cost || choice.Time; ret.push({ "entity": option, "icon": choice.Icon || undefined, "cost": hasCost ? cost : undefined, "tooltip": choice.Tooltip || undefined, - "requiredTechnology": this.GetRequiredTechnology(option), + "requirements": this.GetRequirements(option), }); } return ret; }; Upgrade.prototype.CancelTimer = function() { if (!this.timer) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; }; Upgrade.prototype.IsUpgrading = function() { return !!this.upgrading; }; Upgrade.prototype.GetUpgradingTo = function() { return this.upgrading; }; Upgrade.prototype.WillCheckPlacementRestrictions = function(template) { if (!this.upgradeTemplates[template]) return undefined; // is undefined by default so use X in Y return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]]; }; -Upgrade.prototype.GetRequiredTechnology = function(templateArg) +Upgrade.prototype.GetRequirements = function(templateArg) { let choice = this.upgradeTemplates[templateArg] || templateArg; - if (this.template[choice].RequiredTechnology) - return this.template[choice].RequiredTechnology; + if (this.template[choice].Requirements) + return this.template[choice].Requirements; - if (!("RequiredTechnology" in this.template[choice])) + if (!("Requirements" in this.template[choice])) return undefined; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); let entType = this.template[choice].Entity; if (cmpIdentity) entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv()); let template = cmpTemplateManager.GetTemplate(entType); - return template.Identity.RequiredTechnology || undefined; + return template.Identity.Requirements || undefined; }; Upgrade.prototype.GetResourceCosts = function(template) { if (!this.upgradeTemplates[template]) return undefined; if (this.IsUpgrading() && template == this.GetUpgradingTo()) return clone(this.expendedResources); let choice = this.upgradeTemplates[template]; if (!this.template[choice].Cost) return {}; let costs = {}; for (let r in this.template[choice].Cost) costs[r] = ApplyValueModificationsToEntity("Upgrade/Cost/"+r, +this.template[choice].Cost[r], this.entity); return costs; }; Upgrade.prototype.Upgrade = function(template) { if (this.IsUpgrading() || !this.upgradeTemplates[template]) return false; let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (!cmpPlayer) return false; let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); if (cmpProductionQueue && cmpProductionQueue.HasQueuedProduction()) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("Entity is producing. Cannot start upgrading."), "translateMessage": true }); return false; } this.expendedResources = this.GetResourceCosts(template); if (!cmpPlayer || !cmpPlayer.TrySubtractResources(this.expendedResources)) { this.expendedResources = {}; return false; } this.upgrading = template; this.SetUpgradeAnimationVariant(); // Prevent cheating this.ChangeUpgradedEntityCount(1); if (this.GetUpgradeTime(template) !== 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Upgrade, "UpgradeProgress", 0, UPGRADING_PROGRESS_INTERVAL, { "upgrading": template }); } else this.UpgradeProgress(); return true; }; Upgrade.prototype.CancelUpgrade = function(owner) { if (!this.IsUpgrading()) return; let cmpPlayer = QueryPlayerIDInterface(owner, IID_Player); if (cmpPlayer) cmpPlayer.AddResources(this.expendedResources); this.expendedResources = {}; this.ChangeUpgradedEntityCount(-1); // Do not update visual actor if the animation didn't change. let choice = this.upgradeTemplates[this.upgrading]; if (choice && this.template[choice].Variant) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); } delete this.upgrading; this.CancelTimer(); this.SetElapsedTime(0); }; Upgrade.prototype.GetUpgradeTime = function(templateArg) { let template = this.upgrading || templateArg; let choice = this.upgradeTemplates[template]; if (!choice) return undefined; if (!this.template[choice].Time) return 0; return ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity); }; Upgrade.prototype.GetElapsedTime = function() { return this.elapsedTime; }; Upgrade.prototype.GetProgress = function() { if (!this.IsUpgrading()) return undefined; return this.GetUpgradeTime() == 0 ? 1 : Math.min(this.elapsedTime / 1000.0 / this.GetUpgradeTime(), 1.0); }; Upgrade.prototype.SetElapsedTime = function(time) { this.elapsedTime = time; Engine.PostMessage(this.entity, MT_UpgradeProgressUpdate, null); }; Upgrade.prototype.SetUpgradeAnimationVariant = function() { let choice = this.upgradeTemplates[this.upgrading]; if (!choice || !this.template[choice].Variant) return; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation(this.template[choice].Variant, false, 1.0); }; Upgrade.prototype.UpgradeProgress = function(data, lateness) { if (this.elapsedTime/1000.0 < this.GetUpgradeTime()) { this.SetElapsedTime(this.GetElapsedTime() + UPGRADING_PROGRESS_INTERVAL + lateness); return; } this.CancelTimer(); this.completed = true; this.ChangeUpgradedEntityCount(-1); this.expendedResources = {}; let newEntity = ChangeEntityTemplate(this.entity, this.upgrading); if (newEntity) PlaySound("upgraded", newEntity); }; Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Identity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Identity.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Identity.js (revision 27245) @@ -1,56 +1,59 @@ +Engine.LoadHelperScript("Requirements.js"); Engine.LoadComponentScript("Identity.js"); let cmpIdentity = ConstructComponent(5, "Identity", { "Civ": "iber", "GenericName": "Iberian Skirmisher", "Phenotype": { "_string": "male" }, }); TS_ASSERT_EQUALS(cmpIdentity.GetCiv(), "iber"); TS_ASSERT_EQUALS(cmpIdentity.GetLang(), "greek"); TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "male"); TS_ASSERT_EQUALS(cmpIdentity.GetRank(), ""); TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetClassesList(), []); TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetVisibleClassesList(), []); TS_ASSERT_EQUALS(cmpIdentity.HasClass("CitizenSoldier"), false); TS_ASSERT_EQUALS(cmpIdentity.GetSelectionGroupName(), ""); TS_ASSERT_EQUALS(cmpIdentity.GetGenericName(), "Iberian Skirmisher"); cmpIdentity = ConstructComponent(6, "Identity", { "Civ": "iber", "Lang": "iberian", "Phenotype": { "_string": "female" }, "GenericName": "Iberian Skirmisher", "SpecificName": "Lusitano Ezpatari", "SelectionGroupName": "units/iber/infantry_javelineer_b", "Tooltip": "Basic ranged infantry", "History": "Iberians, especially the Lusitanians, were good at" + " ranged combat and ambushing enemy columns. They throw heavy iron" + " javelins and sometimes even add burning pitch to them, making them" + " good as a cheap siege weapon.", "Rank": "Basic", "Classes": { "_string": "CitizenSoldier Human Organic" }, "VisibleClasses": { "_string": "Javelineer" }, "Icon": "units/iber_infantry_javelineer.png", - "RequiredTechnology": "phase_town" + "Requirements": { + "Techs": "phase_town" + } }); TS_ASSERT_EQUALS(cmpIdentity.GetCiv(), "iber"); TS_ASSERT_EQUALS(cmpIdentity.GetLang(), "iberian"); TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "female"); TS_ASSERT_EQUALS(cmpIdentity.GetRank(), "Basic"); TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetClassesList(), ["CitizenSoldier", "Human", "Organic", "Javelineer", "Basic"]); TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetVisibleClassesList(), ["Javelineer"]); TS_ASSERT_EQUALS(cmpIdentity.HasClass("CitizenSoldier"), true); TS_ASSERT_EQUALS(cmpIdentity.HasClass("Female"), false); TS_ASSERT_EQUALS(cmpIdentity.GetSelectionGroupName(), "units/iber/infantry_javelineer_b"); TS_ASSERT_EQUALS(cmpIdentity.GetGenericName(), "Iberian Skirmisher"); cmpIdentity = ConstructComponent(7, "Identity", { "Phenotype": { "_string": "First Second" }, }); TS_ASSERT_UNEVAL_EQUALS(cmpIdentity.GetPossiblePhenotypes(), ["First", "Second"]); TS_ASSERT(["First", "Second"].indexOf(cmpIdentity.GetPhenotype()) !== -1); cmpIdentity = ConstructComponent(8, "Identity", {}); TS_ASSERT_EQUALS(cmpIdentity.GetPhenotype(), "default"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27245) @@ -1,170 +1,171 @@ Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("Requirements.js"); Engine.LoadHelperScript("ValueModification.js"); Resources = { "BuildSchema": type => { let schema = ""; for (let res of ["food", "metal", "stone", "wood"]) schema += "" + "" + "" + "" + ""; return "" + schema + ""; } }; Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); // Provides `IID_ModifiersManager`, used below. Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below. // What we're testing: Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("Upgrade.js"); // Input (bare minimum needed for tests): let techs = { "alter_tower_upgrade_cost": { "modifications": [ { "value": "Upgrade/Cost/stone", "add": 60.0 }, { "value": "Upgrade/Cost/wood", "multiply": 0.5 }, { "value": "Upgrade/Time", "replace": 90 } ], "affects": ["Tower"] } }; let template = { "Identity": { "Classes": { '@datatype': "tokens", "_string": "Tower" }, "VisibleClasses": { '@datatype': "tokens", "_string": "" } }, "Upgrade": { "Tower": { "Cost": { "stone": "100", "wood": "50" }, "Entity": "structures/{civ}/defense_tower", "Time": "100" } } }; let civCode = "pony"; let playerID = 1; // Usually, the tech modifications would be worked out by the TechnologyManager // with assistance from globalscripts. This test is not about testing the // TechnologyManager, so the modifications (both with and without the technology // researched) are worked out before hand and placed here. let isResearched = false; let templateTechModifications = { "without": {}, "with": { "Upgrade/Cost/stone": [{ "affects": [["Tower"]], "add": 60 }], "Upgrade/Cost/wood": [{ "affects": [["Tower"]], "multiply": 0.5 }], "Upgrade/Time": [{ "affects": [["Tower"]], "replace": 90 }] } }; let entityTechModifications = { "without": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 100 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 50 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 100 } } }, "with": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 160 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 25 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 90 } } } }; /** * Initialise various bits. */ // System Entities: AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": pID => 10 // Called in helpers/player.js::QueryPlayerIDInterface(), as part of Tests T2 and T5. }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetTemplate": () => template, // Called in components/Upgrade.js::ChangeUpgradedEntityCount(). "TemplateExists": (templ) => true }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": () => 1, // Called in components/Upgrade.js::Upgrade(). "CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade(). }); AddMock(SYSTEM_ENTITY, IID_ModifiersManager, { "ApplyTemplateModifiers": (valueName, curValue, template, player) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate() // as part of Tests T2 and T5 below. let mods = isResearched ? templateTechModifications.with : templateTechModifications.without; if (mods[valueName]) return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue); return curValue; }, "ApplyModifiers": (valueName, curValue, ent) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToEntity() // as part of Tests T3, T6 and T7 below. let mods = isResearched ? entityTechModifications.with : entityTechModifications.without; return mods[valueName][ent].newValue; } }); // Init Player: AddMock(10, IID_Player, { "AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade(). "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). }); AddMock(10, IID_Identity, { "GetCiv": () => civCode }); // Create an entity with an Upgrade component: AddMock(20, IID_Ownership, { "GetOwner": () => playerID // Called in helpers/Player.js::QueryOwnerInterface(). }); AddMock(20, IID_Identity, { "GetCiv": () => civCode // Called in components/Upgrade.js::init(). }); AddMock(20, IID_ProductionQueue, { "HasQueuedProduction": () => false }); let cmpUpgrade = ConstructComponent(20, "Upgrade", template.Upgrade); cmpUpgrade.owner = playerID; cmpUpgrade.OnOwnershipChanged({ "to": playerID }); /** * Now to start the test proper * To start with, no techs are researched... */ // T1: Check the cost of the upgrade without a player value being passed (as it would be in the structree). let parsed_template = GetTemplateDataHelper(template, null, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T2: Check the value, with a player ID (as it would be in-session). parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T3: Check that the value is correct within the Update Component. TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 100 }); /** * Tell the Upgrade component to start the Upgrade, * then mark the technology that alters the upgrade cost as researched. */ cmpUpgrade.Upgrade("structures/" + civCode + "/defense_tower"); isResearched = true; // T4: Check that the player-less value hasn't increased... parsed_template = GetTemplateDataHelper(template, null, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T5: ...but the player-backed value has. parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 160, "wood": 25, "time": 90 }); // T6: The upgrade component should still be using the old resource cost (but new time cost) for the upgrade in progress... TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 90 }); // T7: ...but with the upgrade cancelled, it now uses the modified value. cmpUpgrade.CancelUpgrade(playerID); TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 160, "wood": 25, "time": 90 }); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 27245) @@ -1,1851 +1,1848 @@ // 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; } 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 (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.Defeat(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 (!RequirementsHelper.AreRequirementsMet(cmpUpgrade.GetRequirements(cmd.template), player)) { 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; // 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 || !cmpUnitAI.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 const requirements = GetFormationRequirements(formationTemplate); if (!requirements) return false; let count = 0; for (const ent of ents) if (Engine.QueryInterface(ent, IID_UnitAI)?.CanUseFormation(formationTemplate)) ++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/helpers/Requirements.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Requirements.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Requirements.js (revision 27245) @@ -0,0 +1,164 @@ +function RequirementsHelper() {} + +RequirementsHelper.prototype.DEFAULT_RECURSION_DEPTH = 1; + +RequirementsHelper.prototype.EntityRequirementsSchema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + +RequirementsHelper.prototype.TechnologyRequirementsSchema = + "" + + "" + + ""; + +/** + * @param {number} recursionDepth - How deep we recurse. + * @return {string} - A RelaxRNG schema for requirements. + */ +RequirementsHelper.prototype.RequirementsSchema = function(recursionDepth) +{ + return "" + + "" + + this.ChoicesSchema(--recursionDepth) + + ""; +}; + +/** + * @param {number} recursionDepth - How deep we recurse. + * @return {string} - A RelaxRNG schema for chosing requirements. + */ +RequirementsHelper.prototype.ChoicesSchema = function(recursionDepth) +{ + const allAnySchema = recursionDepth > 0 ? "" + + "" + + this.RequirementsSchema(recursionDepth) + + "" + + "" + + this.RequirementsSchema(recursionDepth) + + "" : ""; + + return "" + + "" + + allAnySchema + + this.EntityRequirementsSchema + + this.TechnologyRequirementsSchema + + ""; +}; + +/** + * @param {number} recursionDepth - How deeply recursive we build the schema. + * @return {string} - A RelaxRNG schema for requirements. + */ +RequirementsHelper.prototype.BuildSchema = function(recursionDepth = this.DEFAULT_RECURSION_DEPTH) +{ + return "" + + "" + + "" + + this.ChoicesSchema(recursionDepth) + + "" + + "" + + "" + + "" + + "" + + "" + + ""; +}; + +/** + * @param {Object} template - The requirements template as defined above. + * @param {number} playerID - + * @return {boolean} - + */ +RequirementsHelper.prototype.AreRequirementsMet = function(template, playerID) +{ + if (!template || !Object.keys(template).length) + return true; + + const cmpTechManager = QueryPlayerIDInterface(playerID, IID_TechnologyManager); + return this.AllRequirementsMet(template, cmpTechManager); +}; + +/** + * @param {Object} template - The requirements template for "all". + * @param {component} cmpTechManager - + * @return {boolean} - + */ +RequirementsHelper.prototype.AllRequirementsMet = function(template, cmpTechManager) +{ + for (const requirementType in template) + { + const requirement = template[requirementType]; + if (requirementType === "All" && !this.AllRequirementsMet(requirement, cmpTechManager)) + return false; + if (requirementType === "Any" && !this.AnyRequirementsMet(requirement, cmpTechManager)) + return false; + if (requirementType === "Entities") + { + for (const className in requirement) + { + const entReq = requirement[className]; + if ("Count" in entReq && (!(className in cmpTechManager.classCounts) || cmpTechManager.classCounts[className] < entReq.Count)) + return false; + if ("Variants" in entReq && (!(className in cmpTechManager.typeCountsByClass) || Object.keys(cmpTechManager.typeCountsByClass[className]).length < entReq.Variants)) + return false; + } + } + if (requirementType === "Techs") + for (const tech of requirement.split(" ")) + if (tech[0] === "!" ? cmpTechManager.IsTechnologyResearched(tech.substring(1)) : + !cmpTechManager.IsTechnologyResearched(tech)) + return false; + } + return true; +}; + +/** + * @param {Object} template - The requirements template for "any". + * @param {component} cmpTechManager - + * @return {boolean} - + */ +RequirementsHelper.prototype.AnyRequirementsMet = function(template, cmpTechManager) +{ + for (const requirementType in template) + { + const requirement = template[requirementType]; + if (requirementType === "All" && this.AllRequirementsMet(requirement, cmpTechManager)) + return true; + if (requirementType === "Any" && this.AnyRequirementsMet(requirement, cmpTechManager)) + return true; + if (requirementType === "Entities") + { + for (const className in requirement) + { + const entReq = requirement[className]; + if ("Count" in entReq && className in cmpTechManager.classCounts && cmpTechManager.classCounts[className] >= entReq.Count) + return true; + if ("Variants" in entReq && className in cmpTechManager.typeCountsByClass && Object.keys(cmpTechManager.typeCountsByClass[className]).length >= entReq.Variants) + return true; + } + } + if (requirementType === "Techs") + for (const tech of requirement.split(" ")) + if (tech[0] === "!" ? !cmpTechManager.IsTechnologyResearched(tech.substring(1)) : + cmpTechManager.IsTechnologyResearched(tech)) + return true; + } + return false; +}; + +Engine.RegisterGlobal("RequirementsHelper", new RequirementsHelper()); Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/Requirements.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/helpers/tests/test_Requirements.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Requirements.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Requirements.js (revision 27245) @@ -0,0 +1,718 @@ +Engine.LoadComponentScript("interfaces/PlayerManager.js"); +Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("Requirements.js"); + +const playerID = 1; +const playerEnt = 11; + +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": () => playerEnt +}); + +// First test no requirements. +let template = { +}; + +const met = () => TS_ASSERT(RequirementsHelper.AreRequirementsMet(template, playerID)); +const notMet = () => TS_ASSERT(!RequirementsHelper.AreRequirementsMet(template, playerID)); + +met(); + +// Simple requirements are assumed to be additive. +template = { + "Techs": "phase_city" +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => false +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city" +}); +met(); + +template = { + "Techs": "cartography phase_city" +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => false +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city" +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "cartography" || tech === "phase_town" +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "cartography" || tech === "phase_city" +}); +met(); + + +// Additive requirements (all should to be met). +// Entity requirements. +template = { + "All": { + "Entities": { + "class_1": { + "Count": 1 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": {}, + "typeCountsByClass": {} +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 0 + }, + "typeCountsByClass": {} +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_2": 1 + }, + "typeCountsByClass": {} +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": {} +}); +met(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1 + } + } +}); +met(); + + +template = { + "All": { + "Entities": { + "class_1": { + "Variants": 2 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1 + } + } +}); +notMet(); + +template = { + "All": { + "Entities": { + "class_1": { + "Count": 1, + "Variants": 2 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(); + +template = { + "All": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(); + + +// Technology requirements. +template = { + "All": { + "Techs": "phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => false +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +met(); + +template = { + "All": { + "Techs": "phase_city" + } +}; +notMet(); + +template = { + "All": { + "Techs": "phase_town phase_city" + } +}; +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city" +}); +met(); + +template = { + "All": { + "Techs": "!phase_city" + } +}; +notMet(); + +template = { + "All": { + "Techs": "!phase_town phase_city" + } +}; +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +met(); + + +// Combination of Entity and Technology requirements. +template = { + "All": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => false, + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +notMet(); + + +// Choice requirements (at least one needs to be met). +// Entity requirements. +template = { + "Any": { + "Entities": { + "class_1": { + "Count": 1, + } + }, + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 0 + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 1 + } +}); +met(); + +template = { + "Any": { + "Entities": { + "class_1": { + "Count": 5, + "Variants": 2 + } + }, + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 3, + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "typeCountsByClass": { + "class_1": { + "template_1": 2, + "template_2": 1 + } + } +}); +met(); + + +// Technology requirements. +template = { + "Any": { + "Techs": "phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +met(); + +template = { + "Any": { + "Techs": "phase_town phase_city" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +met(); + +template = { + "Any": { + "Techs": "!phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_city" +}); +met(); + +template = { + "Any": { + "Techs": "!phase_town phase_city" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city" +}); +met(); + + +// Combinational requirements of entities and technologies. +template = { + "Any": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "!phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +met(); + + +// Nested requirements. +template = { + "All": { + "All": { + "Techs": "!phase_town" + }, + "Any": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_city" + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => false, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(); + + +template = { + "Any": { + "All": { + "Techs": "!phase_town" + }, + "Any": { + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_city" + } + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 3 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town" || tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 3 + } + } +}); +met(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => false, + "typeCountsByClass": { + "class_1": { + "template_1": 1, + "template_2": 1 + } + } +}); +met(); + + +// Two levels deep nested. +template = { + "All": { + "Any": { + "All": { + "Techs": "cartography phase_imperial", + }, + "Entities": { + "class_1": { + "Count": 3, + "Variants": 2 + } + }, + "Techs": "phase_city" + }, + "Techs": "!phase_town" + } +}; + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_town", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_city", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "phase_imperial", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +notMet(); + +AddMock(playerEnt, IID_TechnologyManager, { + "classCounts": { + "class_1": 2 + }, + "IsTechnologyResearched": (tech) => tech === "cartography" || tech === "phase_imperial", + "typeCountsByClass": { + "class_1": { + "template_1": 2 + } + } +}); +met(); Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_Requirements.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/templates/gaia/fauna_pig_trainable.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig_trainable.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig_trainable.xml (revision 27245) @@ -1,10 +1,12 @@ - - phase_town + + phase_town + + false Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/spy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/spy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/spy.xml (revision 27245) @@ -1,26 +1,28 @@ 0 0 0 0 0 500 gaia Spy Spy technologies/spy_trader.png - unlock_spies true + + unlock_spies + false 15 0.25 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen/gymnasium.xml (revision 27245) @@ -1,70 +1,72 @@ 200 150 100 8.0 10 2000 athen Gymnasium Gymnasion Train Champions. ConquestCritical CivSpecific Gymnasium -City Town structures/gymnasium.png - phase_town + + phase_town + 40 40 iphicratean_reforms interface/complete/building/complete_gymnasium.xml false 38 40000 0.7 units/{civ}/infantry_marine_archer_b units/{civ}/champion_marine 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit/crannog.xml (revision 27245) @@ -1,52 +1,54 @@ 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 + + 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 structures/britons/crannog.xml structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/apartment.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/apartment.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/apartment.xml (revision 27245) @@ -1,47 +1,49 @@ 2 1.5 50 50 2 1.5 Apartment Building Bet CivSpecific -Village Town structures/apartment.png - phase_town + + phase_town + 10 10 10 + + 2 + structures/carthaginians/fndn_house.xml structures/carthaginians/apartment.xml - - 2 - Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/wallset_short.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/wallset_short.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart/wallset_short.xml (revision 27245) @@ -1,18 +1,20 @@ cart Low Wall - phase_village structures/palisade_wall.png + + phase_village + structures/cart/s_wall_tower structures/cart/s_wall_gate structures/cart/s_wall_long structures/cart/s_wall_medium structures/cart/s_wall_short 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/assembly.xml (revision 27245) @@ -1,78 +1,80 @@ 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 + + phase_city + 80 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/han/academy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/academy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/academy.xml (revision 27245) @@ -1,64 +1,66 @@ Academy 200 0 300 300 10.0 Minister 2000 decay|rubble/rubble_stone_5x5 han Imperial Academy Dìguó Xuéyuàn structures/han/academy Train Champions and research their technologies. -Village City Academy structures/embassy_italic.png - phase_city + + phase_city + -barracks_batch_training -unlock_champion_infantry interface/complete/building/complete_tholos.xml interface/complete/building/complete_tholos.xml 0.8 units/{civ}/champion_infantry_spearman_academy units/{civ}/champion_infantry_crossbowman_academy units/{civ}/champion_cavalry_spearman_academy units/{civ}/champion_chariot_academy structures/han/academy.xml structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre_court.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre_court.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre_court.xml (revision 27245) @@ -1,44 +1,46 @@ ImperialCourt 1.5 1.5 1.5 han Imperial Court Cháotíng Defensive ImperialCourt City CivCentre CivSpecific - phase_city structures/military_settlement.png + + phase_city + 30 -phase_town_{civ} 0.5 units/{civ}/hero_han_xin_horse units/{civ}/hero_liu_bang_horse units/{civ}/hero_wei_qing_chariot structures/han/imperial_court.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/laozigate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/laozigate.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/laozigate.xml (revision 27245) @@ -1,61 +1,63 @@ 120 100 200 12.0 1200 decay|rubble/rubble_stone_4x2 han LaoziGate Lǎozǐ Mén CivSpecific LaoziGate Town structures/paifang.png - phase_town + + phase_town + 20 20 false false 20 30 3 interface/complete/building/complete_iber_monument.xml structures/han/shrine.xml structures/fndn_6x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/wallset_palisade.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/wallset_palisade.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/wallset_palisade.xml (revision 27245) @@ -1,24 +1,26 @@ han Bamboo Stockade Wall off an area. Build in own or neutral territory. -Wall Palisade structures/palisade_wall.png - phase_village + + phase_village + 0.95 0.05 structures/han/palisades_tower structures/han/palisades_gate structures/han/palisades_tower structures/han/palisades_long structures/han/palisades_medium structures/han/palisades_short structures/han/palisades_curve 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_small.xml (revision 27245) @@ -1,58 +1,60 @@ structures/kush_pyramids_economic 120 150 75 15.0 2000 decay|rubble/rubble_stone_4x4 kush Small Pyramid mr -ConquestCritical CivSpecific Village Pyramid - phase_village structures/kush_pyramid_small.png + + phase_village + 30 15 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/maur/tower_double.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/tower_double.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/tower_double.xml (revision 27245) @@ -1,84 +1,118 @@ 2 7 200 1200 Rampart Tower structures/maur/tower_double Higher health tower with ramparts for up to 16 archers. Visibly garrisoned archers recieve a range and armor bonus. Only archers can garrison. Needs the murder holes tech to protect its foot. maur Udarka - phase_city + + phase_city + 40 19.0 - 212.50 + 2 + 12.5 + 0 - 212.52 + 2 + 12.5 + 2 - 212.5-2 + 2 + 12.5 + -2 - 012.52 + 0 + 12.5 + 2 - 012.5-2 + 0 + 12.5 + -2 - -212.50 + -2 + 12.5 + 0 - -212.52 + -2 + 12.5 + 2 - -212.5-2 + -2 + 12.5 + -2 - 2.118.00 + 2.1 + 18.0 + 0 - 2.118.02.1 + 2.1 + 18.0 + 2.1 - 2.118.0-2.1 + 2.1 + 18.0 + -2.1 - 018.02.1 + 0 + 18.0 + 2.1 - 018.0-2.1 + 0 + 18.0 + -2.1 - -2.118.00 + -2.1 + 18.0 + 0 - -2.118.02.1 + -2.1 + 18.0 + 2.1 - -2.118.0-2.1 + -2.1 + 18.0 + -2.1 structures/mauryas/tower_double.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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 27245) @@ -1,64 +1,66 @@ 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 + + phase_town + 20 0 20 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/wallset_siege.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/wallset_siege.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/wallset_siege.xml (revision 27245) @@ -1,25 +1,27 @@ rome Siege Wall Mūrus Circummūnītiōnis Wall off an area. Build in own, neutral, or enemy territory. CivSpecific SiegeWall structures/siege_wall.png - phase_city + + phase_city + structures/rome/siege_wall_tower structures/rome/siege_wall_gate structures/rome/army_camp structures/rome/siege_wall_long structures/rome/siege_wall_medium structures/rome/siege_wall_short 1.00 0.05 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/pyramid_large.xml (revision 27245) @@ -1,64 +1,66 @@ 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 + + phase_city + 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/maur/elephant_stable.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/elephant_stable.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/elephant_stable.xml (revision 27245) @@ -1,13 +1,15 @@ maur Vāraṇaśālā - phase_town CivSpecific -City Town + + phase_town + structures/mauryas/stable_elephant.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/ice_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/ice_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers/ice_house.xml (revision 27245) @@ -1,72 +1,74 @@ - - - - own - Yakhchal - - - 60 - - 100 - 100 - - - - - 12.0 - - - - 800 - decay|rubble/rubble_stone_3x3 - - - pers - Ice House - Yakhchāl - -City Village IceHouse - - CivSpecific - - phase_village - structures/yakhchal.png - - - 20 - 20 - - - - - - - - subterranean_aqueducts - - - - - 1.0 - - 2000 - - - - - interface/complete/building/complete_farmstead.xml - attack/destruction/building_collapse_large.xml - - - - false - 20 - 30000 - - - 20 - - - structures/persians/ice_house.xml - structures/fndn_4x4.xml - - + + + + own + Yakhchal + + + 60 + + 100 + 100 + + + + + 12.0 + + + + 800 + decay|rubble/rubble_stone_3x3 + + + pers + Ice House + Yakhchāl + -City Village IceHouse + + CivSpecific + + structures/yakhchal.png + + phase_village + + + + 20 + 20 + + + + + + + + subterranean_aqueducts + + + + + 1.0 + + 2000 + + + + + interface/complete/building/complete_farmstead.xml + attack/destruction/building_collapse_large.xml + + + + false + 20 + 30000 + + + 20 + + + structures/persians/ice_house.xml + structures/fndn_4x4.xml + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/temple_mars.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/temple_mars.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/temple_mars.xml (revision 27245) @@ -1,26 +1,28 @@ 12.0 10 rome Temple of Mars Aedēs Mārtiālis -Town City TempleOfMars - phase_city + + phase_city + 12 structures/romans/temple_mars.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/tavern.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/tavern.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul/tavern.xml (revision 27245) @@ -1,49 +1,51 @@ 200 100 100 5.0 1500 decay|rubble/rubble_stone_4x4 gaul Tavern Taberna -Village Town structures/embassy_celtic.png - phase_town + + phase_town + 20 20 10 interface/complete/building/complete_broch.xml 30 structures/celts/tavern.xml structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml (revision 27245) @@ -1,47 +1,49 @@ 8.0 Minister han Guān Shǔ -unlock_spies -spy_counter units/{civ}/infantry_spearman_b units/{civ}/infantry_crossbowman_b units/{civ}/cavalry_swordsman_b structures/{civ}/civil_centre_court This greatly increases the health, capture resistance, and garrison capacity of this specific Civic Center. Unlock training of Heroes here and reduce its research and batch training times by half. - phase_city 300 300 upgrading + + phase_city + structures/fndn_8x8.xml structures/han/civil_centre.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/defense_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/defense_tower.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/defense_tower.xml (revision 27245) @@ -1,35 +1,37 @@ 15.0 Minister han Fángyù Tǎ + + + + + 22.0 + structures/{civ}/defense_tower_great This tower has greater range, greater attack, greater health, and is twice as difficult to capture. - phase_city 200 upgrading + + phase_city + - - - - - 22.0 - structures/han/tower_large.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/ministry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/ministry.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/ministry.xml (revision 27245) @@ -1,91 +1,93 @@ ImperialMinistry 3000 5.0 200 0 200 200 8.0 9 0 Unit Support Minister Infantry 1 1 3000 decay|rubble/rubble_stone_6x6 han Imperial Ministry Gōngdiàn CivSpecific ImperialMinistry Town Train the Nine Ministers. Territory root. Research a powerful suite of Administrative technologies. - phase_town structures/imperial_ministry.png + + phase_town + 200 40 40 20 pair_unlock_civil_engineering_han pair_unlock_civil_service_han unlock_spies spy_counter 0.0 0.1 0.1 0.1 2000 true 60 30000 0.8 units/{civ}/support_minister 80 structures/fndn_8x8.xml structures/han/imperial_ministry.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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 27245) @@ -1,69 +1,71 @@ 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 + + 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/temple_amun.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush/temple_amun.xml (revision 27245) @@ -1,55 +1,57 @@ 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 + + phase_city + 2 2 -units/{civ}/support_healer_b units/{civ}/support_healer_e units/{civ}/champion_infantry_amun structures/kushites/temple_amun.xml structures/fndn_9x9.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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 27245) @@ -1,65 +1,67 @@ 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 + + phase_town + 20 0 20 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/rome/army_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 27245) @@ -1,128 +1,130 @@ Bow 10 60 1200 2000 100 1.5 50 false Human outline_border.png outline_border_mask.png 0.175 neutral enemy ArmyCamp ArmyCamp 80 3 15 1 Soldier 3 10.0 1 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 + + phase_city + 100 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/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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 27245) @@ -1,54 +1,56 @@ 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 + + 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_tower_artillery.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_artillery.xml (revision 27245) @@ -1,84 +1,86 @@ Stone 90 0 80 40 0 15 0 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 + + 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_wall.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall.xml (revision 27245) @@ -1,36 +1,38 @@ land-shore Wall 8.0 Wall template_structure_defensive_wall Wall off your town for a stout defense. Wall structures/wall.png - phase_town + + 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_arsenal.xml (revision 27245) @@ -1,76 +1,78 @@ structures/arsenal_repair 180 300 12.0 5 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 + + phase_city + 60 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_forge.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_forge.xml (revision 27245) @@ -1,71 +1,73 @@ 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 + + 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 27245) @@ -1,81 +1,83 @@ structures/corral_garrison 50 100 5.0 8 0.5 Animal Animal 1 4 500 decay|rubble/rubble_stone_3x3 Corral template_structure_resource_corral Raise Domestic Animals for immediate slaughter, or garrison them instead to gain a free trickle of food. Economic Village Corral structures/corral.png - phase_village + + phase_village + 20 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_unit_champion_cavalry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml (revision 27245) @@ -1,65 +1,67 @@ 25 150 80 100 6.0 240 FastMoving Cavalry Champion Cavalry - unlock_champion_cavalry + + unlock_champion_cavalry + 200 15 8 10 5 7 5 20 actor/mounted/movement/walk.xml actor/mounted/movement/walk.xml actor/fauna/death/death_horse.xml interface/alarm/alarm_create_cav.xml 7.0 special/formations/wedge 2 1.4 80 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_fire.xml (revision 27245) @@ -1,63 +1,65 @@ Fire 20 12 50 100 !Ship 30 300 Circular 30 true 600 500 -6 0.85 0.65 0.35 Fire Ship Unrepairable. Gradually loses health. Can only attack Ships. Melee Warship Fireship - phase_town + + phase_town + 128x256/cartouche.png 128x256/cartouche_mask.png ship-small 1.6 1.6 60 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml (revision 27245) @@ -1,22 +1,24 @@ Palisade Wall off an area. Build in own or neutral territory. -Wall Palisade structures/palisade_wall.png - phase_village + + phase_village + structures/palisades_tower structures/palisades_gate structures/palisades_fort structures/palisades_long structures/palisades_medium structures/palisades_short structures/palisades_curve structures/palisades_end 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_temple.xml (revision 27245) @@ -1,76 +1,78 @@ 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 + + phase_town + 60 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_defensive_tower_stone.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_stone.xml (revision 27245) @@ -1,59 +1,61 @@ 0 15 0 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 + + 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 27245) @@ -1,79 +1,81 @@ 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 + + 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_embassy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_embassy.xml (revision 27245) @@ -1,56 +1,58 @@ Embassy 150 12.0 6 Support Infantry Cavalry 2000 decay|rubble/rubble_stone_3x3 Embassy template_structure_military_embassy Town Embassy - phase_town + + phase_town + 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_stable.xml (revision 27245) @@ -1,77 +1,79 @@ 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_01.png - phase_village + + phase_village + 40 10 stable_batch_training 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_cavalry_spearman units/{civ}/champion_cavalry_swordsman units/{civ}/champion_cavalry_javelineer units/{civ}/champion_cavalry_archer units/{civ}/champion_chariot units/{civ}/war_dog 32 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 27245) @@ -1,39 +1,41 @@ Capture 5 4 1000 Field Palisade Wall Human Soldier Champion Champion Unit - phase_city + + phase_city + 8 256x256/arrow.png 256x256/arrow_mask.png voice/{lang}/civ/civ_{phenotype}_attack.xml voice/{lang}/civ/civ_{phenotype}_attack.xml voice/{lang}/civ/civ_{phenotype}_garrison.xml voice/{lang}/civ/civ_{phenotype}_gather.xml voice/{lang}/civ/civ_{phenotype}_walk.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 27245) @@ -1,81 +1,83 @@ Bow 12 45 1000 2000 Ship 3.0 100 2 50 false Ship Human 2 10 1 Infantry Cavalry 2 20 120 60 20 Support Cavalry 800 Light Warship Garrison units for transport and to increase firepower. Deals triple damage against Ships. Ranged Warship Bireme - phase_town + + phase_town + 80 24 12 128x512/cartouche.png 128x512/cartouche_mask.png attack/impact/arrow_impact.xml attack/weapon/bow_attack.xml 1.55 1.55 90 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_stoa.xml (revision 27245) @@ -1,52 +1,54 @@ 150 100 150 10.0 10 0.1 Unit Support Infantry Cavalry 0 2 2500 decay|rubble/rubble_stone_6x4 Stoa template_structure_civic_stoa -ConquestCritical Town Stoa structures/stoa.png - phase_town + + phase_town + 20 30 false 40 65535 40 structures/fndn_8x4.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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_sentry.xml (revision 27245) @@ -1,69 +1,73 @@ 0 9 0 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 + + 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 + + phase_town + 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 27245) @@ -1,76 +1,78 @@ 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 + + phase_town + 60 land 0.2 trader_health trade_gain_01 trade_gain_02 trade_commercial_treaty 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_elephant_stable.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_elephant_stable.xml (revision 27245) @@ -1,64 +1,66 @@ 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 + + phase_city + 40 40 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_range.xml (revision 27245) @@ -1,61 +1,63 @@ 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 + + phase_village + 40 interface/complete/building/complete_range.xml 0.8 units/{civ}/infantry_javelineer_b units/{civ}/infantry_slinger_b units/{civ}/infantry_archer_b units/{civ}/infantry_crossbowman_b units/{civ}/champion_infantry_javelineer units/{civ}/champion_infantry_slinger units/{civ}/champion_infantry_archer units/{civ}/champion_infantry_crossbowman 32 structures/fndn_7x7.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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 27245) @@ -1,99 +1,101 @@ 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 + + phase_city + 200 300 200 structure wonder.png 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_unit_hero.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero.xml (revision 27245) @@ -1,70 +1,72 @@ Capture 10 4 1000 Field Palisade Wall units/heroes/hero_garrison 0 50 100 250 Human Soldier Hero Hero technologies/laurel_wreath.png - phase_city + + phase_city + 400 10 0 0 25 hero 256x256/star.png 256x256/star_mask.png voice/{lang}/civ/civ_{phenotype}_attack.xml voice/{lang}/civ/civ_{phenotype}_attack.xml voice/{lang}/civ/civ_{phenotype}_garrison.xml voice/{lang}/civ/civ_{phenotype}_gather.xml voice/{lang}/civ/civ_{phenotype}_walk.xml interface/alarm/alarm_create_infantry.xml actor/human/movement/walk.xml actor/human/movement/walk.xml actor/human/death/{phenotype}_death.xml Hero 1 100 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 27245) @@ -1,75 +1,77 @@ 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 + + phase_village + 15 5 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_bolt.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower_bolt.xml (revision 27245) @@ -1,81 +1,83 @@ Bolt 100 90 30 0 15 0 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 + + 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 27245) @@ -1,74 +1,76 @@ 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 + + phase_village + 20 gather_wicker_baskets gather_farming_plows gather_farming_training gather_farming_fertilizer gather_farming_seed_drill gather_farming_water_weeding gather_farming_chain_pump 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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_barracks.xml (revision 27245) @@ -1,81 +1,83 @@ structures/xp_trickle 150 200 100 12.0 10 Infantry 2000 decay|rubble/rubble_stone_4x4 Barracks template_structure_military_barracks Train Infantry and research Infantry technologies. Village Barracks structures/barracks.png - phase_village + + phase_village + 40 20 barracks_batch_training 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 32 structures/fndn_6x6.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 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 27245) @@ -1,112 +1,114 @@ Bow 10 60 1200 2000 100 1.5 50 false Human outline_border.png outline_border_mask.png 0.175 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 + + phase_city + 60 120 attack_soldiers_will art_of_war poison_arrows poison_blades 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_special.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_special.xml (revision 27245) @@ -1,38 +1,40 @@ 8.0 5 0.1 Unit Support Infantry Cavalry 0 2 decay|rubble/rubble_stone_6x6 Special Structure City - phase_city + + phase_city + 20 30 3 structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd_domestic_cattle.xml (revision 27245) @@ -1,22 +1,24 @@ - phase_city + + phase_city + 2000 actor/fauna/animal/cattle_order.xml actor/fauna/animal/cattle_death.xml actor/fauna/animal/cattle_trained.xml 0.4 1.4 0.4 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege.xml (revision 27245) @@ -1,74 +1,76 @@ 3 0.0 0.0 2.0 true -Organic Siege Siege - phase_city + + phase_city + pitch-roll 4 4.0 0.0 1 1 25 5 128x256/rounded_rectangle.png 128x256/rounded_rectangle_mask.png attack/siege/ram_move.xml attack/siege/ram_move.xml attack/siege/ram_move.xml attack/siege/ram_trained.xml 4.0 0.5 false large 1 0.75 0.15 5.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/champion_marine.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/champion_marine.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/champion_marine.xml (revision 27245) @@ -1,12 +1,14 @@ Athenian Marine Epibátēs Athēnaîos units/athen/champion_marine.png - iphicratean_reforms + + iphicratean_reforms + units/athenians/infantry_swordsman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 27245) @@ -1,84 +1,86 @@ Bow 12 55 1000 2000 Ship 3.0 100 2 50 false Ship Human 3 13 1 Infantry Cavalry 3 25 200 100 30 Support Soldier Siege 1400 Medium Warship Garrison units for transport and to increase firepower. Deals triple damage against Ships. Ranged Warship Trireme - phase_town + + phase_town + 140 40 20 4 128x512/cartouche.png 128x512/cartouche_mask.png attack/impact/arrow_impact.xml attack/weapon/bow_attack.xml 1.8 1.8 90 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/cavalry_swordsman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/cavalry_swordsman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/cavalry_swordsman_b.xml (revision 27245) @@ -1,21 +1,23 @@ Sword units/athen/cavalry_swordsman_b Greek Cavalry Hippeús units/athen/cavalry_swordsman.png - phase_town + + phase_town + units/athen/cavalry_swordsman_a units/athenians/cavalry_swordsman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 27245) @@ -1,87 +1,89 @@ Stone 150 100 40 2000 5000 40 6 20 false Ship Structure outline_border.png outline_border_mask.png 0.175 1 10 1 StoneThrower 5 30 600 300 10.0 50 Support Soldier Siege 2000 Heavy Warship Garrison units for transport and to increase firepower. Ranged Warship Quinquereme - phase_city + + phase_city + 200 120 60 4 128x512/cartouche.png 128x512/cartouche_mask.png attack/siege/ballist_attack.xml 1.8 1.8 110 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_wallset.xml (revision 27245) @@ -1,26 +1,28 @@ gaia Wall Wall off an area. Wall structures/wall.png - phase_town true + + phase_town + false false false false 0.85 0.05 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_marine_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_marine_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_marine_archer_b.xml (revision 27245) @@ -1,16 +1,18 @@ units/athen/infantry_marine_archer_b Cretan Mercenary Archer Toxótēs Krētikós units/mace/infantry_archer.png - iphicratean_reforms + + iphicratean_reforms + units/athen/infantry_marine_archer_a units/athenians/infantry_archer_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 27245) @@ -1,58 +1,60 @@ 20 100 15 Support Cavalry 240 Merchantman template_unit_ship_merchant Trade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures. -ConquestCritical Trader Bribable - phase_town + + phase_town + 20 128x256/ellipse.png 128x256/ellipse_mask.png 0.75 0.2 12 passive false false ship-small 1.35 1.6 50 true Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml (revision 27245) @@ -1,57 +1,59 @@ 15 250 12 heal_overlay_range.png heal_overlay_range_mask.png 0.35 5 2000 Human 85 -ConquestCritical Healer Healer template_unit_support_healer Basic - phase_town Heal units. + + phase_town + 8 25 150 128x128/plus.png 128x128/plus_mask.png voice/{lang}/civ/civ_{phenotype}_heal.xml interface/alarm/alarm_create_priest.xml 30 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/infantry_javelineer_b.xml (revision 27245) @@ -1,16 +1,18 @@ units/athen/infantry_javelineer_b Thracian Peltast Peltastḗs Thrâx units/athen/infantry_javelinist.png - phase_town + + phase_town + units/athen/infantry_javelineer_a units/athenians/infantry_javelinist_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/cavalry_swordsman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/cavalry_swordsman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/cavalry_swordsman_b.xml (revision 27245) @@ -1,17 +1,19 @@ brit units/brit/cavalry_swordsman_b Eporedos Celtic Cavalry units/brit/cavalry_swordsman.png - phase_town + + phase_town + units/brit/cavalry_swordsman_a units/britons/cavalry_swordsman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_javelineer_iber_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_javelineer_iber_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_javelineer_iber_b.xml (revision 27245) @@ -1,25 +1,27 @@ structures/{civ}/super_dock structures/{civ}/embassy_celtic structures/{civ}/embassy_iberian structures/{civ}/embassy_italic cart Iberian Mercenary Skirmisher Sǝḫīr Kidōn units/cart/infantry_javelineer_iber_b units/cart/infantry_javelinist.png - phase_town + + phase_town + units/cart/infantry_javelineer_iber_a units/iberians/infantry_javelinist_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/ship_merchant.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/ship_merchant.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/ship_merchant.xml (revision 27245) @@ -1,28 +1,30 @@ 8.0 cart Seḥer Trade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures. Carthaginians have +25% sea trading bonus. units/cart/ship_merchant.png - phase_village + + phase_village + 128x512/ellipse.png 128x512/ellipse_mask.png 1.25 structures/carthaginians/merchant_ship.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_infantry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_infantry_swordsman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_infantry_swordsman.xml (revision 27245) @@ -1,12 +1,14 @@ gaul Soliduros units/gaul/champion_infantry.png - unlock_champion_infantry + + unlock_champion_infantry + units/gauls/infantry_swordsman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_crossbowman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_crossbowman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_crossbowman_b.xml (revision 27245) @@ -1,16 +1,18 @@ han units/han/cavalry_crossbowman_b Han Cavalry Crossbowman units/han/cavalry_crossbowman.png - phase_town + + phase_town + units/han/cavalry_crossbowman_a units/han/cavalry_crossbowman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_archer_academy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_archer_academy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_archer_academy.xml (revision 27245) @@ -1,14 +1,16 @@ han Palace Guard Archer Yǔ Lín units/han/champion_infantry_archer units/han/champion_infantry_archer.png - phase_city + + phase_city + units/han/infantry_archer_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_pikeman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_pikeman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_pikeman_b.xml (revision 27245) @@ -1,44 +1,46 @@ Ji 1 1 -structures/wallset_palisade han units/han/infantry_pikeman_b Halberdier Jǐ Bīng units/han/infantry_halberdman.png - phase_town + + phase_town + units/han/infantry_pikeman_a - - units/han/infantry_halberdman_b.xml - -2 -2 special/formations/anti_cavalry + + units/han/infantry_halberdman_b.xml + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/athen/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/infantry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/infantry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/infantry_javelineer_b.xml (revision 27245) @@ -1,23 +1,25 @@ 0.80 0.50 0.20 brit units/brit/infantry_javelineer_b Adretos units/brit/infantry_javelinist.png - phase_town + + phase_town + units/brit/infantry_javelineer_a units/britons/infantry_javelinist_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_iber_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_iber_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_iber_b.xml (revision 27245) @@ -1,22 +1,24 @@ Sword cart Iberian Heavy Cavalry Ḥayyāl Ḥerev Raḫūv units/cart/cavalry_swordsman_iber_b units/cart/cavalry_swordsman.png - phase_town + + phase_town + units/cart/cavalry_swordsman_iber_a units/iberians/cavalry_swordsman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_ital_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_ital_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_ital_b.xml (revision 27245) @@ -1,25 +1,27 @@ structures/{civ}/super_dock structures/{civ}/embassy_celtic structures/{civ}/embassy_iberian structures/{civ}/embassy_italic cart Samnite Swordsman Seḫīr Romaḥ units/cart/infantry_swordsman_ital_b units/cart/infantry_swordsman_2.png - phase_town + + phase_town + units/cart/infantry_swordsman_ital_a units/carthaginians/infantry_swordsman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/champion_fanatic.xml (revision 27245) @@ -1,37 +1,39 @@ 120 100 0 gaul Naked Fanatic Bariogaisatos units/gaul/champion_fanatic.png - phase_town + + phase_town + 12 10 0 -3 -4 1.4 1.4 units/gauls/infantry_spearman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_archer_b.xml (revision 27245) @@ -1,16 +1,18 @@ han units/han/cavalry_archer_b Gōng Qíbīng units/han/cavalry_archer.png - phase_town + + phase_town + units/han/cavalry_archer_a units/han/cavalry_archer_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_chariot_academy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_chariot_academy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_chariot_academy.xml (revision 27245) @@ -1,18 +1,20 @@ 5.0 han Han War Chariot Zhanche Chariot - phase_city units/han/chariot.png + + phase_city + units/han/chariot_archer_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_archer_b.xml (revision 27245) @@ -1,21 +1,23 @@ -structures/wallset_palisade han units/han/infantry_archer_b Shè Shǒu units/han/infantry_archer.png - phase_town + + phase_town + units/han/infantry_archer_a units/han/infantry_archer_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_minister.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_minister.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_minister.xml (revision 27245) @@ -1,107 +1,109 @@ Capture 8 4 1000 Field Palisade Wall Sword 10 3 500 1000 Unit+!Ship units/han_minister_garrison units/han_minister_gathering units/han_minister_building units/han_minister_garrison_ministry 2 15 100 100 200 han Imperial Minister Guānlì Use to boost the efficiency of nearby units and buildings. Garrison within a building to boost the efficiency of its production queue. Only Han buildings can garrison ministers. Organic Human Minister units/han/minister.png - phase_town + + phase_town + 150 10 10 hero 8 2 4 6 128x128/octagram.png 128x128/octagram_mask.png attack/weapon/sword_attack.xml resource/construction/con_wood.xml actor/human/death/{phenotype}_death.xml voice/{lang}/civ/civ_{phenotype}_attack.xml voice/{lang}/civ/civ_{phenotype}_attack.xml voice/{lang}/civ/civ_{phenotype}_garrison.xml voice/{lang}/civ/civ_{phenotype}_gather.xml voice/{lang}/civ/civ_{phenotype}_repair.xml voice/{lang}/civ/civ_{phenotype}_walk.xml actor/human/movement/run.xml interface/alarm/alarm_create_infantry.xml actor/human/movement/walk.xml Minister 40 units/han/minister.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_infantry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_infantry_swordsman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_infantry_swordsman.xml (revision 27245) @@ -1,13 +1,15 @@ brit Brythonic Champion Argos units/brit/champion_infantry.png - unlock_champion_infantry + + unlock_champion_infantry + units/britons/infantry_swordsman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_gaul_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_gaul_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_swordsman_gaul_b.xml (revision 27245) @@ -1,17 +1,19 @@ cart Gallic Mercenary Cavalry Ḥayyāl Ḥerev Raḫūv units/cart/cavalry_swordsman_gaul_b units/cart/cavalry_swordsman_2.png - phase_town + + phase_town + units/cart/cavalry_swordsman_gaul_a units/gauls/cavalry_swordsman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_gaul_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_gaul_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_swordsman_gaul_b.xml (revision 27245) @@ -1,25 +1,27 @@ structures/{civ}/super_dock structures/{civ}/embassy_celtic structures/{civ}/embassy_iberian structures/{civ}/embassy_italic cart Gallic Mercenary Swordsman Seḫīr Ḥerev units/cart/infantry_swordsman_gaul_b units/cart/infantry_swordsman.png - phase_town + + phase_town + units/cart/infantry_swordsman_gaul_a units/gauls/infantry_swordsman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/cavalry_swordsman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/cavalry_swordsman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/cavalry_swordsman_b.xml (revision 27245) @@ -1,16 +1,18 @@ gaul units/gaul/cavalry_swordsman_b Eporedos units/gaul/cavalry_swordsman.png - phase_town + + phase_town + units/gaul/cavalry_swordsman_a units/gauls/cavalry_swordsman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_cavalry_spearman_academy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_cavalry_spearman_academy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_cavalry_spearman_academy.xml (revision 27245) @@ -1,13 +1,15 @@ han Wu Wei Yin Cao Cao Guard units/han/champion_cavalry units/han/champion_cavalry_spearman.png - phase_city + + phase_city + units/han/cavalry_spearman_c_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_spearman_academy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_spearman_academy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_spearman_academy.xml (revision 27245) @@ -1,14 +1,16 @@ han Palace Guard Spearman Hǔ Bēn units/han/champion_infantry_spearman - phase_city units/han/champion_infantry_swordsman.png + + phase_city + units/han/infantry_spearman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_chariot.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_chariot.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/brit/champion_chariot.xml (revision 27245) @@ -1,19 +1,21 @@ 6.0 brit Celtic Chariot Essedon units/brit/champion_chariot Chariot units/brit/champion_chariot.png - unlock_champion_chariots + + unlock_champion_chariots + units/britons/chariot_javelinist_c_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_spearman_ital_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_spearman_ital_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/cavalry_spearman_ital_b.xml (revision 27245) @@ -1,17 +1,19 @@ cart Italic Cavalry Ḥayyāl Romaḥ Raḫūv units/cart/cavalry_spearman_ital_b units/cart/cavalry_spearman.png - phase_town + + phase_town + units/cart/cavalry_spearman_ital_a units/carthaginians/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_slinger_iber_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_slinger_iber_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/infantry_slinger_iber_b.xml (revision 27245) @@ -1,25 +1,27 @@ structures/{civ}/super_dock structures/{civ}/embassy_celtic structures/{civ}/embassy_iberian structures/{civ}/embassy_italic cart Balearic Slinger Qallāʿ Ibušimi units/cart/infantry_slinger_iber_b units/cart/infantry_slinger.png - phase_town + + phase_town + units/cart/infantry_slinger_iber_a units/iberians/infantry_slinger_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/cart/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/infantry_slinger_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/infantry_slinger_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/gaul/infantry_slinger_b.xml (revision 27245) @@ -1,21 +1,23 @@ structures/gaul/assembly gaul units/gaul/infantry_slinger_b Talmoris units/gaul/infantry_slinger.png - phase_town + + phase_town + units/gaul/infantry_slinger_a units/gauls/infantry_slinger_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/cavalry_spearman_b.xml (revision 27245) @@ -1,16 +1,18 @@ han units/han/cavalry_spearman_b Máo Qíbīng units/han/cavalry_spearman.png - phase_town + + phase_town + units/han/cavalry_spearman_a units/han/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_crossbowman_academy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_crossbowman_academy.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/champion_infantry_crossbowman_academy.xml (revision 27245) @@ -1,12 +1,14 @@ han Juezhang units/han/champion_infantry_crossbowman.png - phase_city + + phase_city + units/han/infantry_crossbowman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_swordsman_special_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_swordsman_special_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/han/infantry_swordsman_special_b.xml (revision 27245) @@ -1,21 +1,23 @@ - - - - - -structures/wallset_palisade - - - - han - units/han/infantry_swordsman_b - Dāo Bīng - units/han/infantry_swordsman.png - phase_town - - - units/han/infantry_swordsman_special_a - - - units/han/infantry_swordsman_b.xml - - + + + + + -structures/wallset_palisade + + + + han + units/han/infantry_swordsman_b + Dāo Bīng + units/han/infantry_swordsman.png + + phase_town + + + + units/han/infantry_swordsman_special_a + + + units/han/infantry_swordsman_b.xml + + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/cavalry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/cavalry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/cavalry_spearman_b.xml (revision 27245) @@ -1,16 +1,18 @@ iber units/iber/cavalry_spearman_b Lantzari units/iber/cavalry_spearman.png - phase_town + + phase_town + units/iber/cavalry_spearman_a units/iberians/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/champion_infantry_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/champion_infantry_archer.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/champion_infantry_archer.xml (revision 27245) @@ -1,14 +1,16 @@ kush napatan Noble Archer Hry pdty units/kush/champion_archer.png - unlock_champion_infantry + + unlock_champion_infantry + units/kushites/infantry_archer_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_spearman_b.xml (revision 27245) @@ -1,21 +1,23 @@ structures/iber/monument iber units/iber/infantry_spearman_b Ezkutari units/iber/infantry_spearman.png - phase_town + + phase_town + units/iber/infantry_spearman_a units/iberians/infantry_spearman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_spearman_b.xml (revision 27245) @@ -1,18 +1,20 @@ kush napatan units/kush/cavalry_spearman_b Meroitic Heavy Cavalry Htr units/kush/cavalry_spearman.png - phase_town + + phase_town + units/kush/cavalry_spearman_a units/kushites/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_slinger_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_slinger_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/infantry_slinger_b.xml (revision 27245) @@ -1,21 +1,23 @@ structures/iber/monument iber units/iber/infantry_slinger_b Habailari units/iber/infantry_slinger.png - phase_town + + phase_town + units/iber/infantry_slinger_a units/iberians/infantry_slinger_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_merc_b.xml (revision 27245) @@ -1,39 +1,41 @@ 7.5 kush napatan units/kush/cavalry_javelineer_merc_b Blemmye Desert Raider nhw Bulahau gml Camel units/kush/camel_javelinist.png - phase_town + + phase_town + units/kush/cavalry_javelineer_merc_a actor/fauna/movement/camel_order.xml actor/fauna/death/death_camel.xml 8.5 units/kushites/camel_javelinist_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_javelineer_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_javelineer_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_javelineer_merc_b.xml (revision 27245) @@ -1,26 +1,28 @@ structures/kush/pyramid_large structures/kush/temple_amun structures/kush/camp_blemmye structures/kush/camp_noba kush napatan units/kush/infantry_javelineer_merc_b Noba Skirmisher nhw ʿhȝw Noba units/kush/infantry_javelinist.png - phase_town + + phase_town + units/kush/infantry_javelineer_merc_a units/kushites/infantry_javelinist_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/champion_infantry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/champion_infantry_swordsman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/iber/champion_infantry_swordsman.xml (revision 27245) @@ -1,17 +1,19 @@ Sword iber Leial Ezpatari units/iber/champion_infantry.png - unlock_champion_infantry + + unlock_champion_infantry + units/iberians/infantry_swordsman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/cavalry_javelineer_b.xml (revision 27245) @@ -1,18 +1,20 @@ kush napatan units/kush/cavalry_javelineer_b iry hr ssmwtt Napatan Light Cavalry - phase_village units/kush/cavalry_javelinist.png + + phase_village + units/kush/cavalry_javelineer_a units/kushites/cavalry_javelinist_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_archer_b.xml (revision 27245) @@ -1,26 +1,28 @@ structures/kush/pyramid_large structures/kush/temple_amun structures/kush/camp_blemmye structures/kush/camp_noba kush napatan units/kush/infantry_archer_b Nubian Archer Pdty Nhsyw - phase_village units/kush/infantry_archer.png + + phase_village + units/kush/infantry_archer_a units/kushites/infantry_archer_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_pikeman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_pikeman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_pikeman_b.xml (revision 27245) @@ -1,26 +1,28 @@ structures/kush/pyramid_large structures/kush/temple_amun structures/kush/camp_blemmye structures/kush/camp_noba kush napatan units/kush/infantry_pikeman_b Meroitic Pikeman siȝwrd units/kush/infantry_pikeman.png - phase_town + + phase_town + units/kush/infantry_pikeman_a units/kushites/infantry_pikeman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/cavalry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/cavalry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/cavalry_javelineer_b.xml (revision 27245) @@ -1,18 +1,20 @@ mace greek units/mace/cavalry_javelineer_b Odrysian Skirmish Cavalry Hippakontistḕs Odrysós units/mace/cavalry_javelinist.png - phase_town + + phase_town + units/mace/cavalry_javelineer_a units/macedonians/cavalry_javelinist_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_slinger_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_slinger_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_slinger_b.xml (revision 27245) @@ -1,18 +1,20 @@ mace greek units/mace/infantry_slinger_b Rhodian Slinger Sphendonḗtēs Rhódios units/mace/infantry_slinger.png - phase_town + + phase_town + units/mace/infantry_slinger_a units/macedonians/infantry_slinger_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_infantry_maceman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_infantry_maceman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_infantry_maceman.xml (revision 27245) @@ -1,14 +1,16 @@ maur Warrior Yōddha units/maur/champion_infantry units/maur/champion_maceman.png - unlock_champion_infantry + + unlock_champion_infantry + units/mauryas/infantry_maceman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b_trireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b_trireme.xml (revision 27245) @@ -1,9 +1,11 @@ - equine_transports + + equine_transports + units/pers/cavalry_axeman_a_trireme Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_e_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_e_trireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_e_trireme.xml (revision 27245) @@ -1,6 +1,8 @@ - equine_transports + + equine_transports + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_maceman_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_maceman_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_maceman_merc_b.xml (revision 27245) @@ -1,26 +1,28 @@ structures/kush/pyramid_large structures/kush/temple_amun structures/kush/camp_blemmye structures/kush/camp_noba kush napatan units/kush/infantry_maceman_b Noba Maceman nhw Noba units/kush/infantry_maceman.png - phase_town + + phase_town + units/kush/infantry_maceman_merc_a units/kushites/infantry_maceman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/infantry_archer_b.xml (revision 27245) @@ -1,18 +1,20 @@ mace greek units/mace/infantry_archer_b Cretan Mercenary Archer Toxótēs Krētikós units/mace/infantry_archer.png - phase_town + + phase_town + units/mace/infantry_archer_a units/macedonians/infantry_archer_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_chariot.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_chariot.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/champion_chariot.xml (revision 27245) @@ -1,19 +1,21 @@ 6.0 maur War Chariot Rath units/maur/champion_chariot Chariot units/maur/champion_chariot.png - unlock_champion_chariots + + unlock_champion_chariots + units/mauryas/chariot_archer_c_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/infantry_swordsman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/infantry_swordsman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/infantry_swordsman_b.xml (revision 27245) @@ -1,28 +1,30 @@ Sword structures/maur/palace structures/maur/pillar_ashoka maur units/maur/infantry_swordsman_b Indian Swordsman Khadagdhari units/maur/infantry_swordsman.png - phase_town + + phase_town + units/maur/infantry_swordsman_a units/mauryas/infantry_swordsman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_b.xml (revision 27245) @@ -1,18 +1,20 @@ pers persian units/pers/cavalry_axeman_b Hyrcanian Cavalry Asabāra Varkaniya units/pers/cavalry_axeman.png - phase_town + + phase_town + units/pers/cavalry_axeman_a units/persians/cavalry_axeman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_b_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_b_trireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_b_trireme.xml (revision 27245) @@ -1,9 +1,11 @@ - equine_transports + + equine_transports + units/pers/cavalry_javelineer_a_trireme Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/infantry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/infantry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/infantry_javelineer_b.xml (revision 27245) @@ -1,18 +1,20 @@ pers persian units/pers/infantry_javelineer_b Lydian Auxiliary Pastiš Spardiya units/pers/infantry_javelinist.png - phase_town + + phase_town + units/pers/infantry_javelineer_a units/persians/infantry_javelinist_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_spearman_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_spearman_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_spearman_merc_b.xml (revision 27245) @@ -1,17 +1,19 @@ ptol units/ptol/cavalry_spearman_merc_b Macedonian Settler Cavalry Hippeús Makedonikós units/ptol/cavalry_spearman.png - phase_town + + phase_town + units/ptol/cavalry_spearman_merc_a units/ptolemies/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_swordsman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_swordsman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_swordsman_b.xml (revision 27245) @@ -1,26 +1,28 @@ structures/kush/pyramid_large structures/kush/temple_amun structures/kush/camp_blemmye structures/kush/camp_noba kush napatan units/kush/infantry_swordsman_b Meroitic Swordsman knw hps units/kush/infantry_swordsman.png - phase_town + + phase_town + units/kush/infantry_swordsman_a units/kushites/infantry_swordsman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_swordsman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_swordsman.xml (revision 27245) @@ -1,19 +1,21 @@ Rhomphaia mace greek Thracian Black Cloak Rhomphaiaphoros units/thrac/champion_infantry_swordsman.png - unlock_champion_infantry + + unlock_champion_infantry + units/macedonians/infantry_swordsman_c_thracian.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/cavalry_swordsman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/cavalry_swordsman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/cavalry_swordsman_b.xml (revision 27245) @@ -1,17 +1,19 @@ maur units/maur/cavalry_swordsman_b Indian Raiding Cavalry Aśvārohagaṇaḥ units/maur/cavalry_swordsman.png - phase_town + + phase_town + units/maur/cavalry_swordsman_a units/mauryas/cavalry_swordsman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/hero_chandragupta_infantry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/hero_chandragupta_infantry.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/hero_chandragupta_infantry.xml (revision 27245) @@ -1,24 +1,26 @@ maur Chandragupta Maurya Chandragupta Maurya units/maur/hero_chandragupta.png units/maur/hero_chandragupta This gives Chandragupta Maurya his War Elephant. - phase_city 200 200 + + phase_city + units/mauryas/hero_infantry_swordsman_chandragupta.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_a_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_a_trireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_a_trireme.xml (revision 27245) @@ -1,9 +1,11 @@ - equine_transports + + equine_transports + units/pers/cavalry_axeman_e_trireme Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_a_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_a_trireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_javelineer_a_trireme.xml (revision 27245) @@ -1,9 +1,11 @@ - equine_transports + + equine_transports + units/pers/cavalry_javelineer_e_trireme Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/champion_chariot.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/champion_chariot.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/champion_chariot.xml (revision 27245) @@ -1,19 +1,21 @@ 6.0 pers persian Babylonian Scythed Chariot Raθa Bābiruviya Chariot units/pers/chariot_archer.png - unlock_champion_chariots + + unlock_champion_chariots + units/persians/chariot_archer_e_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_javelineer_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_javelineer_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_javelineer_merc_b.xml (revision 27245) @@ -1,17 +1,19 @@ ptol units/ptol/cavalry_javelineer_merc_b Tarantine Settler Cavalry Hippeús Tarantînos units/hele/tarentine_cavalry_e.png - phase_town + + phase_town + units/ptol/cavalry_javelineer_merc_a units/ptolemies/cavalry_javelinist_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/kush/infantry_spearman_b.xml (revision 27245) @@ -1,26 +1,28 @@ structures/kush/pyramid_large structures/kush/temple_amun structures/kush/camp_blemmye structures/kush/camp_noba kush napatan units/kush/infantry_spearman_b Nubian Spearman iry-rdwy Nhsyw - phase_village units/kush/infantry_spearman.png + + phase_village + units/kush/infantry_spearman_a units/kushites/infantry_spearman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_spearman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_spearman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/champion_infantry_spearman.xml (revision 27245) @@ -1,19 +1,21 @@ mace greek Macedonian Shield Bearer Hypaspistḗs units/mace/champion_infantry_spearman units/mace/hypaspist.png - unlock_champion_infantry + + unlock_champion_infantry + units/mace/champion_infantry_spearman_02 2000 units/macedonians/infantry_spearman_c_a.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/mace/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/elephant_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/elephant_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur/elephant_archer_b.xml (revision 27245) @@ -1,17 +1,19 @@ maur Elephant Archer Vachii Gaja units/maur/elephant_archer_b units/maur/elephant_archer.png - phase_town + + phase_town + units/maur/elephant_archer_a units/mauryas/elephantry_archer_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_archer_b.xml (revision 27245) @@ -1,18 +1,20 @@ pers persian units/pers/cavalry_archer_b Parthian Horse Archer Asabāra Parθava units/pers/cavalry_archer.png - phase_town + + phase_town + units/pers/cavalry_archer_a units/persians/cavalry_archer_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_e_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_e_trireme.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_axeman_e_trireme.xml (revision 27245) @@ -1,6 +1,8 @@ - equine_transports + + equine_transports + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/pers/cavalry_spearman_b.xml (revision 27245) @@ -1,18 +1,20 @@ pers persian units/pers/cavalry_spearman_b Cappadocian Cavalry Asabāra Katpatuka units/pers/cavalry_spearman.png - phase_town + + phase_town + units/pers/cavalry_spearman_a units/persians/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/cavalry_archer_b.xml (revision 27245) @@ -1,38 +1,40 @@ 7.5 ptol Camel units/ptol/cavalry_archer Nabataean Camel Archer Mutsābiq Gamal Nabatu units/ptol/camel_archer.png - phase_village + + phase_village + units/ptol/cavalry_archer_a actor/fauna/movement/camel_order.xml actor/fauna/death/death_camel.xml 8.5 units/ptolemies/camel_archer_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/champion_infantry_pikeman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/champion_infantry_pikeman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/champion_infantry_pikeman.xml (revision 27245) @@ -1,14 +1,16 @@ ptol greek Royal Guard Infantry Phalangitès Agema units/ptol/champion_infantry.png - unlock_champion_infantry + + unlock_champion_infantry + units/ptolemies/infantry_pikeman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_spearman_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_spearman_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_spearman_merc_b.xml (revision 27245) @@ -1,23 +1,25 @@ structures/ptol/lighthouse structures/ptol/library ptol units/ptol/infantry_spearman_merc_b Mercenary Thureos Spearman Thureophóros Misthophóros units/ptol/infantry_spearman_2.png - phase_town + + phase_town + units/ptol/infantry_spearman_merc_a units/ptolemies/infantry_spearman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_spear_gladiator.xml (revision 27245) @@ -1,47 +1,49 @@ structures/rome/army_camp structures/rome/temple_vesta -10 20 5 Gladiator rome latin Gladiator Spearman Hoplomachus Elite units/rome/champion_infantry_gladiator_spear.png - phase_town + + phase_town + -2 Gladiator 1.5 1.5 0.5 units/romans/infantry_gladiator_spearman.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_slinger_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_slinger_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_slinger_b.xml (revision 27245) @@ -1,23 +1,25 @@ structures/ptol/lighthouse structures/ptol/library ptol units/ptol/infantry_slinger_b Judean Slinger Hebraikós Sphendonḗtēs units/ptol/infantry_slinger.png - phase_village + + phase_village + units/ptol/infantry_slinger_a units/ptolemies/infantry_slinger_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/cavalry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/cavalry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/cavalry_javelineer_b.xml (revision 27245) @@ -1,18 +1,20 @@ rome latin Allied Cavalry Eques Socius units/rome/cavalry_javelineer_b units/rome/cavalry_javelinist.png - phase_town + + phase_town + units/rome/cavalry_javelineer_a units/romans/cavalry_javelinist_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_javelineer_b.xml (revision 27245) @@ -1,23 +1,25 @@ structures/ptol/lighthouse structures/ptol/library ptol units/ptol/infantry_javelineer_b Mercenary Thureos Skirmisher Thureophóros Akrobolistḗs units/ptol/infantry_javelinist_merc.png - phase_town + + phase_town + units/ptol/infantry_javelineer_a units/ptolemies/infantry_javelinist_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_swordsman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_swordsman.xml (revision 27245) @@ -1,21 +1,23 @@ 0.85 0.45 rome latin Italic Heavy Infantry Extrāōrdinārius units/rome/champion_infantry_swordsman units/rome/champion_infantry.png - unlock_champion_infantry + + unlock_champion_infantry + units/romans/infantry_swordsman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_archer_b.xml (revision 27245) @@ -1,23 +1,25 @@ structures/ptol/lighthouse structures/ptol/library ptol units/ptol/infantry_archer_b Cretan Mercenary Archer Toxótēs Krētikós units/mace/infantry_archer.png - phase_town + + phase_town + units/ptol/infantry_archer_a units/ptolemies/infantry_archer_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_swordsman_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_swordsman_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/ptol/infantry_swordsman_merc_b.xml (revision 27245) @@ -1,23 +1,25 @@ structures/ptol/lighthouse structures/ptol/library ptol Gallic Mercenary Swordsman Gallikós Mistophorós units/ptol/infantry_swordsman_merc_b units/cart/infantry_swordsman.png - phase_town + + phase_town + units/ptol/infantry_swordsman_merc_a units/ptolemies/infantry_swordsman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/champion_infantry_sword_gladiator.xml (revision 27245) @@ -1,48 +1,50 @@ structures/rome/army_camp structures/rome/temple_vesta -10 20 5 Gladiator rome latin Gladiator Swordsman Murmillo Elite units/rome/champion_infantry_gladiator_sword.png - phase_town + + phase_town + -1 -1 Gladiator 1.4 1.4 0.5 units/romans/infantry_gladiator_swordsman.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_pikeman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_pikeman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_pikeman.xml (revision 27245) @@ -1,14 +1,16 @@ sele greek Silver Shield Argyraspis units/sele/champion_pikeman.png - traditional_army_sele + + traditional_army_sele + units/seleucids/infantry_pikeman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_swordsman_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_swordsman_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_swordsman_merc_b.xml (revision 27245) @@ -1,23 +1,25 @@ Rhomphaia sele greek units/sele/infantry_swordsman_merc_b Thracian Mercenary Swordsman Rhomphaiaphoros Thrakikós units/sele/infantry_swordsman.png - phase_town + + phase_town + units/sele/infantry_swordsman_merc_a units/seleucids/infantry_swordsman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house + Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/infantry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/infantry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/rome/infantry_spearman_b.xml (revision 27245) @@ -1,35 +1,37 @@ structures/rome/army_camp structures/rome/temple_vesta 0.85 0.45 rome latin units/rome/infantry_spearman_b Veteran Spearman Triārius units/rome/infantry_spearman.png - phase_town + + phase_town + units/rome/infantry_spearman_a special/formations/anti_cavalry units/romans/infantry_spearman_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_chariot.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_chariot.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_chariot.xml (revision 27245) @@ -1,19 +1,21 @@ 6.0 sele greek Scythed Chariot Drepanèphoros Chariot units/sele/champion_chariot.png - unlock_champion_chariots + + unlock_champion_chariots + units/seleucids/chariot_archer_c_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_javelineer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_javelineer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_javelineer_b.xml (revision 27245) @@ -1,18 +1,20 @@ sele greek units/sele/infantry_javelineer_b Arab Javelineer Pezakontistès Aravikós units/sele/infantry_javelinist.png - phase_village + + phase_village + units/sele/infantry_javelineer_a units/seleucids/infantry_javelinist_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/champion_infantry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/champion_infantry_swordsman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/champion_infantry_swordsman.xml (revision 27245) @@ -1,30 +1,32 @@ structures/spart/syssiton -structures/{civ}/wallset_stone 25 spart greek Skiritai Commando Ékdromos Skirítēs Elite units/spart/champion_infantry_sword.png - phase_town + + phase_town + 3 units/spartans/infantry_swordsman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_spearman_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_spearman_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_spearman_merc_b.xml (revision 27245) @@ -1,18 +1,20 @@ sele greek units/sele/cavalry_spearman_merc_b Companion Cavalry Hippos Hetairike units/sele/cavalry_spearman_merc.png - phase_town + + phase_town + units/sele/cavalry_spearman_merc_a units/seleucids/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_archer_merc_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_archer_merc_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/infantry_archer_merc_b.xml (revision 27245) @@ -1,18 +1,20 @@ sele greek units/sele/infantry_archer_merc_b Syrian Archer Toxótēs Syrías units/sele/infantry_archer.png - phase_town + + phase_town + units/sele/infantry_archer_merc_a units/seleucids/infantry_archer_b.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/cavalry_spearman_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/cavalry_spearman_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/spart/cavalry_spearman_b.xml (revision 27245) @@ -1,18 +1,20 @@ spart greek units/spart/cavalry_spearman_b Greek Allied Cavalry Hippeús Symmakhikós units/spart/cavalry_spearman.png - phase_town + + phase_town + units/spart/cavalry_spearman_a units/spartans/cavalry_spearman_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_archer_b.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_archer_b.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/cavalry_archer_b.xml (revision 27245) @@ -1,18 +1,20 @@ sele greek units/sele/cavalry_archer_b Dahae Horse Archer Hippotoxotès Dahae units/pers/cavalry_archer.png - phase_town + + phase_town + units/sele/cavalry_archer_a units/persians/cavalry_archer_b_m.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_swordsman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_swordsman.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/champion_infantry_swordsman.xml (revision 27245) @@ -1,14 +1,16 @@ sele greek Romanized Heavy Swordsman Thorakitès Rhomaïkós units/sele/champion_swordsman.png - reformed_army_sele + + reformed_army_sele + units/seleucids/infantry_swordsman_c.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/support_female_citizen_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/support_female_citizen_house.xml (revision 27244) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/sele/support_female_citizen_house.xml (revision 27245) @@ -1,9 +1,11 @@ 30 - unlock_females_house + + unlock_females_house +