Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 24044) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 24045) @@ -1,563 +1,568 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ "Code", "Culture", "Name", "Emblem", "History", "Music", "Factions", "CivBonuses", "TeamBonuses", "Structures", "StartEntities", "Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); if (!selectableOnly || data.SelectableInGameSetup) civData[data.Code] = data; } return civData; } /** * @return {string[]} - All the classes for this identity template. */ function GetIdentityClasses(template) { let classString = ""; if (template.Classes && template.Classes._string) classString += " " + template.Classes._string; if (template.VisibleClasses && template.VisibleClasses._string) classString += " " + template.VisibleClasses._string; if (template.Rank) classString += " " + template.Rank; return classString.length > 1 ? classString.substring(1).split(" ") : []; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : []; } /** * Check if a given list of classes matches another list of classes. * Useful f.e. for checking identity classes. * * @param classes - List of the classes to check against. * @param match - Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2. * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed. * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed. * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise. */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // Transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (let sublist of match) { // If the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) || (c[0] != "!" && classes.indexOf(c) != -1))) return true; } return false; } /** * Gets the value originating at the value_path as-is, with no modifiers applied. * * @param {Object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {Object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {Object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers && modifiers[mod_key]) current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {Object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {Object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.Resistance) { // Don't show Foundation resistance. ret.resistance = {}; if (template.Resistance.Entity) { if (template.Resistance.Entity.Damage) { ret.resistance.Damage = {}; for (let damageType in template.Resistance.Entity.Damage) ret.resistance.Damage[damageType] = getEntityValue("Resistance/Entity/Damage/" + damageType); } if (template.Resistance.Entity.Capture) ret.resistance.Capture = getEntityValue("Resistance/Entity/Capture"); // ToDo: Resistance against StatusEffects. } } 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] = { "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus"), }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Projectile) ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true"; Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); if (template.Attack[type].Splash) { ret.attack[type].splash = { "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, }; Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", }; Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) { let aura = auraTemplates[auraID]; ret.auras[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": aura.radius || null }; } } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.PopulationBonus) ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "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.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); ret.nativeCiv = template.Identity.Civ; } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; ret.speed.run = getEntityValue("UnitMotion/WalkSpeed"); if (template.UnitMotion.RunMultiplier) ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier"); } if (template.Upgrade) { ret.upgrades = []; for (let upgradeName in template.Upgrade) { let upgrade = template.Upgrade[upgradeName]; let cost = {}; if (upgrade.Cost) for (let res in upgrade.Cost) cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); if (upgrade.Time) cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); ret.upgrades.push({ "entity": upgrade.Entity, "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, "requiredTechnology": upgrade.RequiredTechnology || undefined }); } } if (template.ProductionQueue) { ret.techCostMultiplier = {}; for (let res in template.ProductionQueue.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "_fortress", "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap }; if (template.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(/\s+/); } if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length, "angle": +(template.WallPiece.Orientation || 1) * Math.PI, "indent": +(template.WallPiece.Indent || 0), "bend": +(template.WallPiece.Bend || 0) * Math.PI }; return ret; } /** * Get basic information about a technology template. * @param {Object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects, "replaces": template.replaces }; } /** * Get information about a technology template. * @param {Object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } 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 24044) +++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 24045) @@ -1,1003 +1,1015 @@ var g_TooltipTextFormats = { "unit": { "font": "sans-10", "color": "orange" }, "header": { "font": "sans-bold-13" }, "body": { "font": "sans-13" }, "comma": { "font": "sans-12" }, "nameSpecificBig": { "font": "sans-bold-16" }, "nameSpecificSmall": { "font": "sans-bold-12" }, "nameGeneric": { "font": "sans-bold-16" } }; /** * 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", "populationBonus", "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) }); } /** * 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)); // TODO: Status effects resistance. return 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 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": 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 => { let template = g_StatusEffectsMetadata.augment(x, applyStatusTemplate[x]); return unitFont(translateWithContext("status effect", template.StatusName)); }).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 attackLabel = sprintf(headerFont(translate("%(attackType)s")), { "attackType": attackType }); let attackTypeTemplate = template.attack[attackType]; 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) { let status_template = g_StatusEffectsMetadata.augment(status, attackTypeTemplate.ApplyStatus[status]); statusEffectsDetails.push("\n" + g_Indent + g_Indent + getStatusEffectsTooltip(status_template, 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(template, applier) { let tooltipAttributes = []; if (applier && template.ApplierTooltip) tooltipAttributes.push(translate(template.ApplierTooltip)); else if (!applier && template.ReceiverTooltip) tooltipAttributes.push(translate(template.ReceiverTooltip)); 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) return sprintf(translate("%(statusName)s: %(statusInfo)s %(stackability)s"), { "statusName": headerFont(translateWithContext("status effect", template.StatusName)), "statusInfo": tooltipAttributes.join(commaFont(translate(", "))), "stackability": getStatusEffectStackabilityTooltip(template) }); return sprintf(translate("%(statusName)s: %(statusInfo)s"), { "statusName": headerFont(translateWithContext("status effect", template.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) { if (!template.garrisonHolder) return ""; let tooltips = [ 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(translate("Health")), "second": unitFont(translate("second")), }) ); return tooltips.join(commaFont(translate(", "))); } 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) { return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Number of repairers:")), "details": entState.repairable.numBuilders }) + "\n" + (entState.repairable.numBuilders ? 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.", Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew)), { "second": Math.round(entState.repairable.buildTime.timeRemaining - entState.repairable.buildTime.timeRemainingNew) }) : 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) })); } function getBuildTimeTooltip(entState) { return sprintf(translate("%(label)s %(details)s"), { "label": headerFont(translate("Number of builders:")), "details": entState.foundation.numBuilders }) + "\n" + (entState.foundation.numBuilders ? 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.", Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew)), { "second": Math.round(entState.foundation.buildTime.timeRemaining - entState.foundation.buildTime.timeRemainingNew) }) : 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) })); } /** * 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) { 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()) // Population bonus is shown in the tooltip if (type != "populationBonus" && 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 ""; // Average the resource rates (TODO: distinguish between subtypes) 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 and skew the results if (subtype !== "ruins") types.push(resource.code + "." + subtype); let [rate, count] = types.reduce((sum, t) => { let r = template.resourceGatherRates[t]; return [sum[0] + (r > 0 ? r : 0), sum[1] + (r > 0 ? 1 : 0)]; }, [0, 0]); if (rate > 0) rates[resource.code] = +(rate / count).toFixed(2); } 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] }) ).join(" ") }); } /** * Returns the resources this entity supplies in the specified entity's tooltip */ function getResourceSupplyTooltip(template) { if (!template.supply) return ""; let supply = template.supply; let type = supply.type[0] == "treasure" ? supply.type[1] : supply.type[0]; // 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(type), // Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource. "amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞") }); } 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) }) }); } /** * 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) { if (technologyEnabled) return ""; return sprintf(translate("Requires %(technology)s"), { "technology": getEntityNames(GetTechnologyData(requiredTechnology, civ)) }); } /** * Returns the population bonus information to display in the specified entity's construction button tooltip. */ function getPopulationBonusTooltip(template) { let popBonus = ""; if (template.cost && template.cost.populationBonus) popBonus = sprintf(translate("%(label)s %(populationBonus)s"), { "label": headerFont(translate("Population Bonus:")), "populationBonus": template.cost.populationBonus }); return popBonus; } /** * 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": resources[resource] })); return coloredText( '[font="sans-bold-13"]' + translate("Insufficient resources:") + '[/font]', "red") + " " + formatted.join(" "); } function getSpeedTooltip(template) { if (!template.speed) return ""; let walk = template.speed.walk.toFixed(1); let run = template.speed.run.toFixed(1); if (walk == 0 && run == 0) return ""; 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")) }) }); } 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": translate(auras[auraID].name) })), "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; return sprintf(translate("%(specificName)s (%(genericName)s)"), { "specificName": template.name.specific, "genericName": template.name.generic }); } + function getEntityNamesFormatted(template) { if (!template.name.specific) return setStringTags(template.name.generic, g_TooltipTextFormats.nameSpecificBig); // Translation: Example: "Epibátēs Athēnaîos [font="sans-bold-16"](Athenian Marine)[/font]" return sprintf(translate("%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s"), { "specificName": setStringTags(template.name.specific[0], g_TooltipTextFormats.nameSpecificBig) + setStringTags(template.name.specific.slice(1).toUpperCase(), g_TooltipTextFormats.nameSpecificSmall), "genericName": template.name.generic, "fontStart": '[font="' + g_TooltipTextFormats.nameGeneric.font + '"]', "fontEnd": '[/font]' }); } 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."); } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js (revision 24044) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js (revision 24045) @@ -1,66 +1,67 @@ /** * This class contains code common to the Structure Tree, Template Viewer, and any other "Reference Page" that may be added in the future. */ class ReferencePage { constructor() { this.civData = loadCivData(true, false); this.TemplateLoader = new TemplateLoader(); this.TemplateLister = new TemplateLister(this.TemplateLoader); this.TemplateParser = new TemplateParser(this.TemplateLoader); this.activeCiv = this.TemplateLoader.DefaultCiv; this.currentTemplateLists = {}; } setActiveCiv(civCode) { if (civCode == this.TemplateLoader.DefaultCiv) return; this.activeCiv = civCode; this.currentTemplateLists = this.TemplateLister.compileTemplateLists(this.activeCiv, this.civData); this.TemplateParser.deriveModifications(this.activeCiv); this.TemplateParser.derivePhaseList(this.currentTemplateLists.techs.keys(), this.activeCiv); } /** * Concatanates the return values of the array of passed functions. * * @param {Object} template * @param {array} textFunctions * @param {string} joiner * @return {string} The built text. */ static buildText(template, textFunctions=[], joiner="\n") { return textFunctions.map(func => func(template)).filter(tip => tip).join(joiner); } } ReferencePage.prototype.IconPath = "session/portraits/"; /** * List of functions that get the statistics of any template or entity, * formatted in such a way as to appear in a tooltip. * * The functions listed are defined in gui/common/tooltips.js */ ReferencePage.prototype.StatsFunctions = [ + getResourceDropsiteTooltip, getHealthTooltip, getAttackTooltip, getHealerTooltip, getResistanceTooltip, getGarrisonTooltip, getProjectilesTooltip, getSpeedTooltip, getGatherTooltip, getResourceSupplyTooltip, getPopulationBonusTooltip, getResourceTrickleTooltip, getLootTooltip ]; Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 24044) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 24045) @@ -1,1192 +1,1194 @@ /** * 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 = g_EntityCommands[command].getInfo(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 = g_IsObserver && data.item.name == "focus-rally" || controlsPlayer(data.player) && (data.item.name != "delete" || data.unitEntStates.some(state => !isUndeletable(state))); 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.AllyCommand = { "getMaxNumberOfItems": function() { return 2; }, "conflictsWith": ["Command"], "getItems": function(unitEntStates) { let commands = []; for (let command in g_AllyEntityCommands) for (let state of unitEntStates) { let info = g_AllyEntityCommands[command].getInfo(state); if (info) { info.name = command; commands.push(info); break; } } 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 performAllyCommand(data.unitEntStates[0].id, data.item.name); }; data.countDisplay.caption = data.item.count || ""; data.button.enabled = !!data.item.count; let grayscale = data.button.enabled ? "" : "grayscale:"; data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon; let size = data.button.size; // relative to the center ( = 50%) size.rleft = 50; size.rright = 50; // offset from the center calculation, count on square buttons, so size.bottom is the width too size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1); size.right = size.left + size.bottom; data.button.size = size; return true; } }; g_SelectionPanels.Construction = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function() { return getAllBuildableEntitiesFromSelection(); }, "setupButton": function(data) { let template = GetTemplateData(data.item, data.player); if (!template) return false; let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": data.player }); let neededResources; if (template.cost) neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, 1), "player": data.player }); data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); }; data.button.onPressRight = function() { showTemplateDetails(data.item); }; let tooltips = [ getEntityNamesFormatted, getVisibleEntityClassesFormatted, getAurasTooltip, getEntityTooltip ].map(func => func(template)); tooltips.push( getEntityCostTooltip(template, data.player), + getResourceDropsiteTooltip(template), getGarrisonTooltip(template), getPopulationBonusTooltip(template), showTemplateViewerOnRightClickTooltip(template) ); let limits = getEntityLimitAndCount(data.playerState, data.item); tooltips.push( formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; if (!technologyEnabled || limits.canBeAddedCount == 0) { data.button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; } else if (neededResources) { data.button.enabled = false; modifier += resourcesToAlphaMask(neededResources) + ":"; } else data.button.enabled = controlsPlayer(data.player); if (template.icon) data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; g_SelectionPanels.Formation = { "getMaxNumberOfItems": function() { return 15; }, "rowLength": 5, "conflictsWith": ["Garrison"], "getItems": function(unitEntStates) { if (unitEntStates.some(state => !hasClass(state, "Unit"))) return []; if (unitEntStates.every(state => !state.identity || !state.identity.hasSomeFormation)) return []; if (!g_AvailableFormations.has(unitEntStates[0].player)) g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player)); return g_AvailableFormations.get(unitEntStates[0].player).filter(formation => unitEntStates.some(state => !!state.identity && state.identity.formations.indexOf(formation) != -1)); }, "setupButton": function(data) { if (!g_FormationsInfo.has(data.item)) g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); let formationOk = data.item == "special/formations/null" || 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); }; let formationInfo = g_FormationsInfo.get(data.item); let tooltip = translate(formationInfo.name); 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.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 = g_ViewedPlayer == data.player || g_ViewedPlayer == entState.player; data.button.enabled = canUngarrison && controlsPlayer(g_ViewedPlayer); data.button.tooltip = (canUngarrison || g_IsObserver ? 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) { let queue = []; let foundNew = true; for (let i = 0; foundNew; ++i) { foundNew = false; for (let state of unitEntStates) { if (!state.production || !state.production.queue[i]) continue; queue.push({ "producingEnt": state.id, "queuedItem": state.production.queue[i] }); foundNew = true; } } 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); }; let tooltip = getEntityNames(template); if (queuedItem.neededSlots) { tooltip += "\n" + coloredText(translate("Insufficient population capacity:"), "red"); tooltip += "\n" + sprintf(translate("%(population)s %(neededSlots)s"), { "population": resourceIcon("population"), "neededSlots": queuedItem.neededSlots }); } data.button.tooltip = tooltip; data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : ""; // 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")); let guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]"); let size = guiObject.size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left)); guiObject.size = size; if (template.icon) data.icon.sprite = "stretched:session/portraits/" + template.icon; data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Research = { "getMaxNumberOfItems": function() { return 10; }, "rowLength": 10, "getItems": function(unitEntStates) { let ret = []; if (unitEntStates.length == 1) return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret : unitEntStates[0].production.technologies.map(tech => ({ "tech": tech, "techCostMultiplier": unitEntStates[0].production.techCostMultiplier, "researchFacilityId": unitEntStates[0].id })); for (let state of unitEntStates) { if (!state.production || !state.production.technologies) continue; // Remove the techs we already have in ret (with the same name and techCostMultiplier) let filteredTechs = state.production.technologies.filter( tech => tech != null && !ret.some( item => (item.tech == tech || item.tech.pair && tech.pair && item.tech.bottom == tech.bottom && item.tech.top == tech.top) && Object.keys(item.techCostMultiplier).every( k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k]) )); if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() && getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2)) ret = ret.concat(filteredTechs.map(tech => ({ "tech": tech, "techCostMultiplier": state.production.techCostMultiplier, "researchFacilityId": state.id }))); } 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; for (let res in template.cost) template.cost[res] *= data.item.techCostMultiplier[res]; let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": template.cost, "player": player }); let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", { "tech": tech, "player": player }); let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]"); let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]"); let tooltips = [ getEntityNamesFormatted, getEntityTooltip, getEntityCostTooltip, showTemplateViewerOnRightClickTooltip ].map(func => func(template)); if (!requirementsPassed) { let tip = template.requirementsTooltip; let reqs = template.reqs; for (let req of reqs) { if (!req.entities) continue; let entityCounts = []; for (let entity of req.entities) { let current = 0; switch (entity.check) { case "count": current = playerState.classCounts[entity.class] || 0; break; case "variants": current = playerState.typeCountsByClass[entity.class] ? Object.keys(playerState.typeCountsByClass[entity.class]).length : 0; break; } let remaining = entity.number - current; if (remaining < 1) continue; entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), { "number": remaining, "class": 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); button.onPressRight = (t => function() { showTemplateDetails( t, GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv); })(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 (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() { changePrimarySelectionGroup(data.item.key, false); }; data.button.onPressRight = function() { changePrimarySelectionGroup(data.item.key, true); }; if (template.icon) data.icon.sprite = "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Stance = { "getMaxNumberOfItems": function() { return 5; }, "getItems": function(unitEntStates) { if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal"))) return []; return unitEntStates[0].unitAI.selectableStances; }, "setupButton": function(data) { let unitIds = data.unitEntStates.map(state => state.id); data.button.onPress = function() { performStance(unitIds, data.item); }; data.button.tooltip = getStanceDisplayName(data.item) + "\n" + "[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]"; data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", { "ents": unitIds, "stance": data.item }); data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png"; data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Training = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function() { return getAllTrainableEntitiesFromSelection(); }, "setupButton": function(data) { let template = GetTemplateData(data.item, data.player); if (!template) return false; let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": data.player }); let unitIds = data.unitEntStates.map(status => status.id); let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingStatus(unitIds, data.item, data.playerState); let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; let neededResources; if (template.cost) neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, trainNum), "player": data.player }); data.button.onPress = function() { if (!neededResources) addTrainingToQueue(unitIds, data.item, data.playerState); }; data.button.onPressRight = function() { showTemplateDetails(data.item); }; 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)); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") tooltips = tooltips.concat([ getHealthTooltip, getAttackTooltip, getHealerTooltip, getResistanceTooltip, getGarrisonTooltip, getProjectilesTooltip, - getSpeedTooltip + getSpeedTooltip, + getResourceDropsiteTooltip ].map(func => func(template))); tooltips.push(showTemplateViewerOnRightClickTooltip()); tooltips.push( formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch), getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; if (!technologyEnabled || limits.canBeAddedCount == 0) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } else { data.button.enabled = controlsPlayer(data.player); if (neededResources) modifier = resourcesToAlphaMask(neededResources) + ":"; } if (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 technologyEnabled = true; if (data.item.requiredTechnology) technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.item.requiredTechnology, "player": data.player }); let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.item, data.unitEntStates.length), "player": data.player }); let limits = getEntityLimitAndCount(data.playerState, data.item.entity); let progress = data.unitEntStates[0].upgrade.progress || 0; let isUpgrading = data.unitEntStates[0].upgrade.template == data.item.entity; let tooltip; if (!progress) { let tooltips = []; if (data.item.tooltip) tooltips.push(sprintf(translate("Upgrade to %(name)s. %(tooltip)s"), { "name": template.name.generic, "tooltip": translate(data.item.tooltip) })); else tooltips.push(sprintf(translate("Upgrade to %(name)s."), { "name": template.name.generic })); tooltips.push( getEntityCostComponentsTooltipString(data.item, undefined, data.unitEntStates.length), formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources), showTemplateViewerOnRightClickTooltip()); tooltip = tooltips.filter(tip => tip).join("\n"); data.button.onPress = function() { upgradeEntity(data.item.entity); }; } else if (isUpgrading) { tooltip = translate("Cancel Upgrading"); data.button.onPress = function() { cancelUpgradeEntity(); }; } else { tooltip = translate("Cannot upgrade when the entity is already upgrading."); data.button.onPress = function() {}; } data.button.enabled = controlsPlayer(data.player); data.button.tooltip = tooltip; data.button.onPressRight = function() { showTemplateDetails(data.item.entity); }; let modifier = ""; if (!isUpgrading) if (progress || !technologyEnabled || limits.canBeAddedCount == 0 && !hasSameRestrictionCategory(data.item.entity, data.unitEntStates[0].template)) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } else if (neededResources) { data.button.enabled = false; modifier = resourcesToAlphaMask(neededResources) + ":"; } data.icon.sprite = modifier + "stretched:session/" + (data.item.icon || "portraits/" + template.icon); data.countDisplay.caption = data.unitEntStates.length > 1 ? data.unitEntStates.length : ""; let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]"); if (isUpgrading) { let size = progressOverlay.size; size.top = size.left + Math.round(progress * (size.right - size.left)); progressOverlay.size = size; } progressOverlay.hidden = !isUpgrading; 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) { 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", "AllyCommand", "Queue", "Selection", ]; 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 24044) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_crannog.xml (revision 24045) @@ -1,48 +1,48 @@ 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. Dropsite for food, wood, stone, and metal. Train Citizens, construct Ships, and research technologies. Garrison Soldiers for additional arrows. + 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. Naval structures/crannog.png phase_town true 0.0 units/{civ}_infantry_spearman_b units/{civ}_infantry_slinger_b units/{civ}_cavalry_javelineer_b units/{civ}_ship_fishing units/{civ}_ship_merchant units/{civ}_ship_bireme units/{civ}_ship_trireme -phase_town_{civ} ship structures/britons/crannog.xml structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 24044) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 24045) @@ -1,144 +1,144 @@ FemaleCitizen 140 190 100 0.0 12.0 0.0 72.0 0.0 1200 2000 0 75.0 1.5 9.81 false Human outline_border.png outline_border_mask.png 0.175 3 1 own neutral CivilCentre CivilCentre 200 2500 5.0 20 500 500 500 500 8.0 20 0.1 Unit Support Infantry Cavalry 1 1 3000 decay|rubble/rubble_stone_6x6 Civic Center template_structure_civic_civil_centre - Build in own or neutral territory. Acquire large tracts of territory. Territory root. Dropsite for food, wood, stone, and metal. Train Citizens and research technologies. Garrison Soldiers for additional arrows. + Build in own or neutral territory. Acquire large tracts of territory. Territory root. Train Citizens and research technologies. Garrison Soldiers for additional arrows. CivCentre Defensive CivilCentre structures/civic_centre.png 200 100 100 100 0.8 units/{civ}_support_female_citizen phase_town_{civ} phase_city_{civ} unlock_spies spy_counter 5 5 5 15 3 food wood stone metal true interface/complete/building/complete_civ_center.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml true 140 10000 90 structures/fndn_8x8.xml 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 24044) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 24045) @@ -1,73 +1,73 @@ FemaleCitizen 60 100 100 Farmstead 300 45 100 8.0 900 decay|rubble/rubble_stone_4x4 Farmstead template_structure_economic_farmstead - Dropsite for food. Research food gathering technologies. + Research food gathering technologies. DropsiteFood Village Farmstead structures/farmstead.png phase_village 20 gather_wicker_baskets gather_farming_plows gather_farming_training gather_farming_fertilizer gather_farming_harvester food true interface/complete/building/complete_farmstead.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml false 20 30000 20 structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 24044) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 24045) @@ -1,84 +1,84 @@ FemaleCitizen 60 100 100 Storehouse 300 40 100 8.0 800 decay|rubble/rubble_stone_3x3 Storehouse template_structure_economic_storehouse - Dropsite for wood, stone, and metal. Research gathering technologies. + Research gathering technologies. DropsiteWood DropsiteMetal DropsiteStone Village Storehouse structures/storehouse.png phase_village 20 gather_lumbering_ironaxes gather_lumbering_strongeraxes gather_lumbering_sharpaxes gather_mining_servants gather_mining_serfs gather_mining_slaves gather_mining_wedgemallet gather_mining_shaftmining gather_mining_silvermining gather_capacity_basket gather_capacity_wheelbarrow gather_capacity_carts wood stone metal true interface/complete/building/complete_storehouse.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml false 20 30000 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 24044) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 24045) @@ -1,83 +1,83 @@ own ally neutral shore Dock 5 150 200 8.0 2500 decay|rubble/rubble_stone_4x4 Dock template_structure_military_dock - Build upon a shoreline in own, neutral, or allied territory. Dropsite for food, wood, stone, and metal. Establish trade routes. Construct Ships and research Ship technologies. + Build upon a shoreline in own, neutral, or allied territory. Establish trade routes. Construct Ships and research Ship technologies. Economic Naval Trade Village Dock structures/dock.png 40 land naval 0.2 true 0.0 0.8 units/{civ}_ship_fishing units/{civ}_ship_merchant units/{civ}_ship_bireme units/{civ}_ship_trireme gather_capacity_fishing gather_fishing_net training_naval_architects armor_ship_reinforcedhull armor_ship_hypozomata armor_ship_hullsheathing ship food wood stone metal true interface/complete/building/complete_dock.xml 40 structures/fndn_4x4_dock.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml (revision 24044) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml (revision 24045) @@ -1,85 +1,84 @@ units/elephant_worker 20 150 8.0 300 maur - Elephant Worker Elephant Karmākara Gaja + Elephant units/maur_support_elephant.png - Mobile dropsite for food, wood, stone, and metal. 50 25 10 10 10 pitch 5 8 10 food wood stone metal false 128x256/ellipse.png 128x256/ellipse_mask.png actor/fauna/animal/elephant_attack_order.xml actor/fauna/animal/elephant_order.xml actor/fauna/animal/elephant_attack.xml actor/fauna/animal/elephant_death.xml actor/fauna/animal/elephant_trained.xml 9.0 false large 0.6 50 units/mauryas/support_elephant.xml