Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 18153) +++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 18154) @@ -1,545 +1,545 @@ -const g_CostDisplayNames = { +const g_CostDisplayIcons = { "food": '[icon="iconFood"]', "wood": '[icon="iconWood"]', "stone": '[icon="iconStone"]', "metal": '[icon="iconMetal"]', "population": '[icon="iconPopulation"]', "time": '[icon="iconTime"]' }; const g_TooltipTextFormats = { "unit": ['[font="sans-10"][color="orange"]', '[/color][/font]'], "header": ['[font="sans-bold-13"]', '[/font]'], "body": ['[font="sans-13"]', '[/font]'] }; function damageValues(dmg) { if (!dmg) return [0, 0, 0]; return [dmg.hack || 0, dmg.pierce || 0, dmg.crush || 0]; } function damageTypeDetails(dmg) { if (!dmg) return '[font="sans-12"]' + translate("(None)") + '[/font]'; let dmgArray = []; if (dmg.hack) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { "damage": dmg.hack.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1] })); if (dmg.pierce) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { "damage": dmg.pierce.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1] })); if (dmg.crush) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { "damage": dmg.crush.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1] })); return dmgArray.join(translate(", ")); } function attackRateDetails(entState, type) { // Either one arrow shot by UnitAI, let time = entState.attack[type].repeatTime / 1000; let timeString = sprintf(translatePlural("%(time)s %(second)s", "%(time)s %(second)s", time), { "time": time, "second": g_TooltipTextFormats.unit[0] + translatePlural("second", "seconds", time) + g_TooltipTextFormats.unit[1] }); // or multiple arrows shot by BuildingAI if (!entState.buildingAI) return timeString; let arrows = entState.buildingAI.arrowCount; let arrowString = sprintf(translatePlural("%(arrowcount)s %(arrow)s", "%(arrowcount)s %(arrow)s", arrows), { "arrowcount": arrows, "arrow": g_TooltipTextFormats.unit[0] + translatePlural("arrow", "arrows", arrows) + g_TooltipTextFormats.unit[1] }); return sprintf(translate("%(arrowString)s / %(timeString)s"), { "arrowString": arrowString, "timeString": timeString }); } // Converts an armor level into the actual reduction percentage function armorLevelToPercentageString(level) { return (100 - Math.round(Math.pow(0.9, level) * 100)) + "%"; // return sprintf(translate("%(armorPercentage)s%"), { armorPercentage: (100 - Math.round(Math.pow(0.9, level) * 100)) }); // Not supported by our sprintf implementation. } function getArmorTooltip(dmg) { let label = g_TooltipTextFormats.header[0] + translate("Armor:") + g_TooltipTextFormats.header[1]; if (!dmg) return sprintf(translate("%(label)s %(details)s"), { "label": label, "details": '[font="sans-12"]' + translate("(None)") + '[/font]' }); let dmgArray = []; if (dmg.hack) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), { "damage": dmg.hack.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1], "armorPercentage": '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { "armorPercentage": armorLevelToPercentageString(dmg.hack) }) + '[/font]' })); if (dmg.pierce) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), { "damage": dmg.pierce.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1], "armorPercentage": '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { "armorPercentage": armorLevelToPercentageString(dmg.pierce) }) + '[/font]' })); if (dmg.crush) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), { "damage": dmg.crush.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1], "armorPercentage": '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { "armorPercentage": armorLevelToPercentageString(dmg.crush) }) + '[/font]' })); return sprintf(translate("%(label)s %(details)s"), { "label": label, "details": dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]') }); } function damageTypesToText(dmg) { if (!dmg) return '[font="sans-12"]' + translate("(None)") + '[/font]'; let dmgArray = []; if (dmg.hack) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { "damage": dmg.hack.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Hack") + g_TooltipTextFormats.unit[1] })); if (dmg.pierce) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { "damage": dmg.pierce.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Pierce") + g_TooltipTextFormats.unit[1] })); if (dmg.crush) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { "damage": dmg.crush.toFixed(1), "damageType": g_TooltipTextFormats.unit[0] + translate("Crush") + g_TooltipTextFormats.unit[1] })); return dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]'); } function getAttackTypeLabel(type) { if (type === "Charge") return translate("Charge Attack:"); if (type === "Melee") return translate("Melee Attack:"); if (type === "Ranged") return translate("Ranged Attack:"); if (type === "Capture") return translate("Capture Attack:"); warn(sprintf("Internationalization: Unexpected attack type found with code ‘%(attackType)s’. This attack type must be internationalized.", { "attackType": type })); return translate("Attack:"); } function getAttackTooltip(template) { let attacks = []; if (!template.attack) return ""; let rateLabel = g_TooltipTextFormats.header[0] + (template.buildingAI ? translate("Interval:") : translate("Rate:")) + g_TooltipTextFormats.header[1]; for (let type in template.attack) { if (type == "Slaughter") continue; // Slaughter is not a real attack, so do not show it. if (type == "Charge") continue; // Charging isn't implemented yet and shouldn't be displayed. let rate = sprintf(translate("%(label)s %(details)s"), { "label": rateLabel, "details": attackRateDetails(template, type) }); let attackLabel = g_TooltipTextFormats.header[0] + getAttackTypeLabel(type) + g_TooltipTextFormats.header[1]; if (type == "Capture") { attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), { "attackLabel": attackLabel, "details": template.attack[type].value, "rate": rate })); continue; } if (type != "Ranged") { attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), { "attackLabel": attackLabel, "details": damageTypesToText(template.attack[type]), "rate": rate })); continue; } let realRange = template.attack[type].elevationAdaptedRange; let range = Math.round(template.attack[type].maxRange); let rangeLabel = g_TooltipTextFormats.header[0] + translate("Range:") + g_TooltipTextFormats.header[1]; let relativeRange = Math.round((realRange - range)); let meters = g_TooltipTextFormats.unit[0] + translatePlural("meter", "meters", range) + g_TooltipTextFormats.unit[1]; if (relativeRange) // show if it is non-zero attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rangeLabel)s %(rangeString)s (%(relative)s), %(rate)s"), { "attackLabel": attackLabel, "details": damageTypesToText(template.attack[type]), "rangeLabel": rangeLabel, "rangeString": sprintf( translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), { "range": range, "meters": meters }), "relative": relativeRange > 0 ? "+" + relativeRange : relativeRange, "rate": rate })); else attacks.push(sprintf(translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(rangeString)s, %(rate)s"), { "attackLabel": attackLabel, "damageTypes": damageTypesToText(template.attack[type]), "rangeLabel": rangeLabel, "rangeString": sprintf( translatePlural("%(range)s %(meters)s", "%(range)s %(meters)s", range), { "range": range, "meters": meters }), rate: rate })); } return attacks.join("\n"); } function getRepairRateTooltip(rate) { return "\n" + sprintf(translate("%(repairRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), { "repairRateLabel": g_TooltipTextFormats.header[0] + translate("Repair Rate:") + g_TooltipTextFormats.header[1], "value": Math.round(rate * 10) / 10, "health": g_TooltipTextFormats.unit[0] + translate("health") + g_TooltipTextFormats.unit[1], "second": g_TooltipTextFormats.unit[0] + translate("second") + g_TooltipTextFormats.unit[1], "worker": g_TooltipTextFormats.unit[0] + translate("worker") + g_TooltipTextFormats.unit[1] }); } function getBuildRateTooltip(rate) { return "\n" + sprintf(translate("%(buildRateLabel)s %(value)s %(health)s / %(second)s / %(worker)s"), { "buildRateLabel": g_TooltipTextFormats.header[0] + translate("Build Rate:") + g_TooltipTextFormats.header[1], "value": Math.round(rate * 10) / 10, "health": g_TooltipTextFormats.unit[0] + translate("health") + g_TooltipTextFormats.unit[1], "second": g_TooltipTextFormats.unit[0] + translate("second") + g_TooltipTextFormats.unit[1], "worker": g_TooltipTextFormats.unit[0] + translate("worker") + g_TooltipTextFormats.unit[1] }); } /** * Translates a cost component identifier as they are used internally * (e.g. "population", "food", etc.) to proper display names. */ -function getCostComponentDisplayName(costComponentName) +function getCostComponentDisplayIcon(costComponentName) { - if (costComponentName in g_CostDisplayNames) - return g_CostDisplayNames[costComponentName]; + if (costComponentName in g_CostDisplayIcons) + return g_CostDisplayIcons[costComponentName]; warn(sprintf("The specified cost component, ‘%(component)s’, is not currently supported.", { "component": costComponentName })); return ""; } /** * Multiplies the costs for a template by a given batch size. */ function multiplyEntityCosts(template, trainNum) { let totalCosts = {}; for (let r in template.cost) totalCosts[r] = Math.floor(template.cost[r] * trainNum); return totalCosts; } /** * Helper function for getEntityCostTooltip. */ function getEntityCostComponentsTooltipString(template, trainNum, entity) { if (!trainNum) trainNum = 1; let totalCosts = multiplyEntityCosts(template, trainNum); totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", { "entity": entity, "batchSize": trainNum }) : 1)); let costs = []; - for (let type in g_CostDisplayNames) + for (let type in g_CostDisplayIcons) if (totalCosts[type]) costs.push(sprintf(translate("%(component)s %(cost)s"), { - "component": getCostComponentDisplayName(type), + "component": getCostComponentDisplayIcon(type), "cost": totalCosts[type] })); return costs; } /** * 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 = {}; // Initialize the acceptable types for '$x to $y $resource' mode. for (let resource in wallTypes[0].cost) 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) { let resourceMin = Math.min.apply(Math, resourceCount[resource]); let resourceMax = Math.max.apply(Math, resourceCount[resource]); // 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": getCostComponentDisplayName(resource), + "resourceIcon": getCostComponentDisplayIcon(resource), "minimum": resourceMin, "maximum": resourceMax })); } } 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, trainNum, entity) { // 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); let templateMedium = GetTemplateData(template.wallSet.templates.medium); let templateShort = GetTemplateData(template.wallSet.templates.short); let templateTower = GetTemplateData(template.wallSet.templates.tower); 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) return getEntityCostComponentsTooltipString(template, trainNum, entity).join(" "); return ""; } /** * 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 = "\n" + sprintf(translate("%(label)s %(populationBonus)s"), { "label": g_TooltipTextFormats.header[0] + translate("Population Bonus:") + g_TooltipTextFormats.header[1], "populationBonus": template.cost.populationBonus }); return popBonus; } /** * Returns a message with the amount of each resource needed to create an entity. */ function getNeededResourcesTooltip(resources) { let formatted = []; for (let resource in resources) formatted.push(sprintf(translate("%(component)s %(cost)s"), { - "component": '[font="sans-12"]' + getCostComponentDisplayName(resource) + '[/font]', + "component": '[font="sans-12"]' + getCostComponentDisplayIcon(resource) + '[/font]', "cost": resources[resource] })); return '\n\n[font="sans-bold-13"][color="red"]' + translate("Insufficient resources:") + '[/color][/font]\n' + formatted.join(" "); } function getSpeedTooltip(template) { if (!template.speed) return ""; let label = g_TooltipTextFormats.header[0] + translate("Speed:") + g_TooltipTextFormats.header[1]; let speeds = []; if (template.speed.walk) speeds.push(sprintf(translate("%(speed)s %(movementType)s"), { "speed": Math.round(template.speed.walk), "movementType": g_TooltipTextFormats.unit[0] + translate("Walk") + g_TooltipTextFormats.unit[1] })); if (template.speed.run) speeds.push(sprintf(translate("%(speed)s %(movementType)s"), { "speed": Math.round(template.speed.run), "movementType": g_TooltipTextFormats.unit[0] + translate("Run") + g_TooltipTextFormats.unit[1] })); return sprintf(translate("%(label)s %(speeds)s"), { "label": label, "speeds": speeds.join(translate(", ")) }); } function getHealerTooltip(template) { if (!template.healer) return ""; let healer = [ sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.HP), { "label": g_TooltipTextFormats.header[0] + translate("Heal:") + g_TooltipTextFormats.header[1], "val": template.healer.HP, // Translation: Short for hitpoints (that are healed in one healing action) "unit": g_TooltipTextFormats.unit[0] + translatePlural("HP", "HP", template.healer.HP) + g_TooltipTextFormats.unit[1] }), sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.Range), { "label": g_TooltipTextFormats.header[0] + translate("Range:") + g_TooltipTextFormats.header[1], "val": template.healer.Range, "unit": g_TooltipTextFormats.unit[0] + translatePlural("meter", "meters", template.healer.Range) + g_TooltipTextFormats.unit[1] }), sprintf(translatePlural("%(label)s %(val)s %(unit)s", "%(label)s %(val)s %(unit)s", template.healer.Rate/1000), { "label": g_TooltipTextFormats.header[0] + translate("Rate:") + g_TooltipTextFormats.header[1], "val": template.healer.Rate/1000, "unit": g_TooltipTextFormats.unit[0] + translatePlural("second", "seconds", template.healer.Rate/1000) + g_TooltipTextFormats.unit[1] }) ]; return healer.join(translate(", ")); } function getAurasTooltip(template) { if (!template.auras) return ""; let txt = ""; for (let aura in template.auras) txt += '\n' + sprintf(translate("%(auralabel)s %(aurainfo)s"), { "auralabel": g_TooltipTextFormats.header[0] + sprintf(translate("%(auraname)s:"), { "auraname": translate(template.auras[aura].name) }) + g_TooltipTextFormats.header[1], "aurainfo": g_TooltipTextFormats.body[0] + translate(template.auras[aura].description) + g_TooltipTextFormats.body[1] }); return txt; } function getEntityNames(template) { if (template.name.specific) { if (template.name.generic && template.name.specific != template.name.generic) return sprintf(translate("%(specificName)s (%(genericName)s)"), { "specificName": template.name.specific, "genericName": template.name.generic }); return template.name.specific; } if (template.name.generic) return template.name.generic; warn("Entity name requested on an entity without a name, specific or generic."); return translate("???"); } function getEntityNamesFormatted(template) { let names = ""; let generic = template.name.generic; let specific = template.name.specific; if (specific) { // drop caps for specific name names += '[font="sans-bold-16"]' + specific[0] + '[/font]' + '[font="sans-bold-12"]' + specific.slice(1).toUpperCase() + '[/font]'; if (generic) names += '[font="sans-bold-16"] (' + generic + ')[/font]'; } else if (generic) names = '[font="sans-bold-16"]' + generic + "[/font]"; else names = "???"; return names; } function getVisibleEntityClassesFormatted(template) { let r = ""; if (template.visibleIdentityClasses && template.visibleIdentityClasses.length) { r += '\n' + g_TooltipTextFormats.header[0] + translate("Classes:") + g_TooltipTextFormats.header[1]; let classes = []; for (let c of template.visibleIdentityClasses) classes.push(translate(c)); r += ' ' + g_TooltipTextFormats.body[0] + classes.join(translate(", ")) + g_TooltipTextFormats.body[1]; } return r; } Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 18153) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 18154) @@ -1,457 +1,468 @@ function layoutSelectionSingle() { Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false; Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; } function layoutSelectionMultiple() { Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; } function getResourceTypeDisplayName(resourceType) { let resourceCode = resourceType.generic; if (resourceCode == "treasure") return getLocalizedResourceName(resourceType.specific, "firstWord"); else return getLocalizedResourceName(resourceCode, "firstWord"); } // Updates the health bar of garrisoned units function updateGarrisionHealthBar(entState, selection) { if (!entState.garrisonHolder) return; // Summing up the Health of every single unit let totalGarrisionHealth = 0; let maxGarrisionHealth = 0; for (let selEnt of selection) { let selEntState = GetEntityState(selEnt); if (selEntState.garrisonHolder) for (let ent of selEntState.garrisonHolder.entities) { let state = GetEntityState(ent); totalGarrisionHealth += state.hitpoints || 0; maxGarrisionHealth += state.maxHitpoints || 0; } } // Configuring the health bar let healthGarrison = Engine.GetGUIObjectByName("healthGarrison"); healthGarrison.hidden = totalGarrisionHealth <= 0; if (totalGarrisionHealth > 0) { let healthBarGarrison = Engine.GetGUIObjectByName("healthBarGarrison"); let healthSize = healthBarGarrison.size; healthSize.rtop = 100-100*Math.max(0, Math.min(1, totalGarrisionHealth / maxGarrisionHealth)); healthBarGarrison.size = healthSize; healthGarrison.tooltip = sprintf(translate("%(label)s %(current)s / %(max)s"), { "label": "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]", "current": Math.ceil(totalGarrisionHealth), "max": Math.ceil(maxGarrisionHealth) }); } } // Fills out information that most entities have function displaySingle(entState) { // Get general unit and player data let template = GetTemplateData(entState.template); let specificName = template.name.specific; let genericName = template.name.generic; // If packed, add that to the generic name (reduces template clutter) if (genericName && template.pack && template.pack.state == "packed") genericName = sprintf(translate("%(genericName)s — Packed"), { "genericName": genericName }); let playerState = g_Players[entState.player]; let civName = g_CivData[playerState.civ].Name; let civEmblem = g_CivData[playerState.civ].Emblem; let playerName = playerState.name; let playerColor = playerState.color.r + " " + playerState.color.g + " " + playerState.color.b + " 128"; // Indicate disconnected players by prefixing their name if (g_Players[entState.player].offline) playerName = sprintf(translate("\\[OFFLINE] %(player)s"), { "player": playerName }); // Rank if (entState.identity && entState.identity.rank && entState.identity.classes) { Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), { "rank": translateWithContext("Rank", entState.identity.rank) }); Engine.GetGUIObjectByName("rankIcon").sprite = getRankIconSprite(entState); Engine.GetGUIObjectByName("rankIcon").hidden = false; } else { Engine.GetGUIObjectByName("rankIcon").hidden = true; Engine.GetGUIObjectByName("rankIcon").tooltip = ""; } // Hitpoints Engine.GetGUIObjectByName("healthSection").hidden = !entState.hitpoints; if (entState.hitpoints) { let unitHealthBar = Engine.GetGUIObjectByName("healthBar"); let healthSize = unitHealthBar.size; healthSize.rright = 100*Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints)); unitHealthBar.size = healthSize; if (entState.foundation && entState.visibility == "visible" && entState.foundation.numBuilders !== 0) { // logic comes from Foundation component. let speed = Math.pow(entState.foundation.numBuilders, 0.7); let timeLeft = (1.0 - entState.foundation.progress / 100.0) * template.cost.time; let timeToCompletion = Math.ceil(timeLeft/speed); Engine.GetGUIObjectByName("health").tooltip = sprintf(translatePlural("This foundation will be completed in %(seconds)s second.", "This foundation will be completed in %(seconds)s seconds.", timeToCompletion), { "seconds": timeToCompletion }); } else Engine.GetGUIObjectByName("health").tooltip = ""; Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), { "hitpoints": Math.ceil(entState.hitpoints), "maxHitpoints": Math.ceil(entState.maxHitpoints) }); } // CapturePoints Engine.GetGUIObjectByName("captureSection").hidden = !entState.capturePoints; if (entState.capturePoints) { let setCaptureBarPart = function(playerID, startSize) { let unitCaptureBar = Engine.GetGUIObjectByName("captureBar["+playerID+"]"); let sizeObj = unitCaptureBar.size; sizeObj.rleft = startSize; let size = 100*Math.max(0, Math.min(1, entState.capturePoints[playerID] / entState.maxCapturePoints)); sizeObj.rright = startSize + size; unitCaptureBar.size = sizeObj; unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[playerID].color, 128); unitCaptureBar.hidden=false; return startSize + size; }; // first handle the owner's points, to keep those points on the left for clarity let size = setCaptureBarPart(entState.player, 0); for (let i in entState.capturePoints) if (i != entState.player) size = setCaptureBarPart(i, size); Engine.GetGUIObjectByName("captureStats").caption = sprintf(translate("%(capturePoints)s / %(maxCapturePoints)s"), { "capturePoints": Math.ceil(entState.capturePoints[entState.player]), "maxCapturePoints": Math.ceil(entState.maxCapturePoints) }); } // Experience Engine.GetGUIObjectByName("experience").hidden = !entState.promotion; if (entState.promotion) { let experienceBar = Engine.GetGUIObjectByName("experienceBar"); let experienceSize = experienceBar.size; experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req))); experienceBar.size = experienceSize; if (entState.promotion.curr < entState.promotion.req) Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), { "experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]", "current": Math.floor(entState.promotion.curr), "required": entState.promotion.req }); else Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), { "experience": "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]", "current": Math.floor(entState.promotion.curr) }); } // Resource stats Engine.GetGUIObjectByName("resourceSection").hidden = !entState.resourceSupply; if (entState.resourceSupply) { let resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol sprintf(translate("%(amount)s / %(max)s"), { "amount": Math.ceil(+entState.resourceSupply.amount), "max": entState.resourceSupply.max }); let resourceType = getResourceTypeDisplayName(entState.resourceSupply.type); let unitResourceBar = Engine.GetGUIObjectByName("resourceBar"); let resourceSize = unitResourceBar.size; resourceSize.rright = entState.resourceSupply.isInfinite ? 100 : 100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max)); unitResourceBar.size = resourceSize; Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), { "resource": resourceType }); Engine.GetGUIObjectByName("resourceStats").caption = resources; if (entState.hitpoints) Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("captureSection").size; else Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("healthSection").size; } // Resource carrying if (entState.resourceCarrying && entState.resourceCarrying.length) { // We should only be carrying one resource type at once, so just display the first let carried = entState.resourceCarrying[0]; Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+carried.type+".png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { "amount": carried.amount, "max": carried.max }); Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = ""; } // Use the same indicators for traders else if (entState.trader && entState.trader.goods.amount) { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+entState.trader.goods.type+".png"; let totalGain = entState.trader.goods.amount.traderGain; if (entState.trader.goods.amount.market1Gain) totalGain += entState.trader.goods.amount.market1Gain; if (entState.trader.goods.amount.market2Gain) totalGain += entState.trader.goods.amount.market2Gain; Engine.GetGUIObjectByName("resourceCarryingText").caption = totalGain; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Gain: %(gain)s"), { "gain": getTradingTooltip(entState.trader.goods.amount) }); } // And for number of workers else if (entState.foundation && entState.visibility == "visible") { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " "; if (entState.foundation.numBuilders !== 0) { let speedup = Math.pow((entState.foundation.numBuilders+1)/entState.foundation.numBuilders, 0.7); let timeLeft = (1.0 - entState.foundation.progress / 100.0) * template.cost.time; let timeSpeedup = Math.ceil(timeLeft - timeLeft/speedup); Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translatePlural("Number of builders.\nTasking another to this foundation would speed construction up by %(speedup)s second.", "Number of builders.\nTasking another to this foundation would speed construction up by %(speedup)s seconds.", timeSpeedup), { "speedup": timeSpeedup }); } else Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Number of builders."); } else if (entState.repairable && entState.repairable.numBuilders > 0 && entState.visibility == "visible") { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.repairable.numBuilders + " "; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Number of builders."); } else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints) && entState.visibility == "visible") { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { "amount": entState.resourceSupply.numGatherers, "max": entState.resourceSupply.maxGatherers }) + " "; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers"); } else { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = true; Engine.GetGUIObjectByName("resourceCarryingText").hidden = true; } // Set Player details Engine.GetGUIObjectByName("specific").caption = specificName; Engine.GetGUIObjectByName("player").caption = playerName; Engine.GetGUIObjectByName("playerColorBackground").sprite = "color: " + playerColor; if (genericName !== specificName) Engine.GetGUIObjectByName("generic").caption = sprintf(translate("(%(genericName)s)"), { "genericName": genericName }); else Engine.GetGUIObjectByName("generic").caption = ""; if ("gaia" != playerState.civ) { Engine.GetGUIObjectByName("playerCivIcon").sprite = "stretched:grayscale:" + civEmblem; Engine.GetGUIObjectByName("player").tooltip = civName; } else { Engine.GetGUIObjectByName("playerCivIcon").sprite = ""; Engine.GetGUIObjectByName("player").tooltip = ""; } // Icon image // TODO: we should require all entities to have icons Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "bkFillBlack"; let armorString = getArmorTooltip(entState.armour); // Attack and Armor Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = entState.attack ? (getAttackTooltip(entState) + "\n" + armorString) : armorString; // Repair Rate if (entState.repairRate) Engine.GetGUIObjectByName("attackAndArmorStats").tooltip += getRepairRateTooltip(entState.repairRate); // Build Rate if (entState.buildRate) Engine.GetGUIObjectByName("attackAndArmorStats").tooltip += getBuildRateTooltip(entState.buildRate); // Icon Tooltip let iconTooltip = ""; if (genericName) iconTooltip = "[font=\"sans-bold-16\"]" + genericName + "[/font]"; if (template.visibleIdentityClasses && template.visibleIdentityClasses.length) { iconTooltip += "\n[font=\"sans-bold-13\"]" + translate("Classes:") + "[/font] "; iconTooltip += "[font=\"sans-13\"]" + template.visibleIdentityClasses.map(c => translate(c)).join(", ") + "[/font]"; } if (template.auras) iconTooltip += getAurasTooltip(template); if (template.tooltip) iconTooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]"; Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltip; // Unhide Details Area Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false; Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; } // Fills out information for multiple entities function displayMultiple(selection) { let averageHealth = 0; let maxHealth = 0; let maxCapturePoints = 0; let capturePoints = (new Array(g_MaxPlayers + 1)).fill(0); let playerID = 0; + let totalResourcesCarried = {}; for (let i = 0; i < selection.length; ++i) { let entState = GetEntityState(selection[i]); if (!entState) continue; playerID = entState.player; // trust that all selected entities have the same owner if (entState.hitpoints) { averageHealth += entState.hitpoints; maxHealth += entState.maxHitpoints; } if (entState.capturePoints) { maxCapturePoints += entState.maxCapturePoints; capturePoints = entState.capturePoints.map((v, i) => v + capturePoints[i]); } + + if (entState.resourceCarrying && entState.resourceCarrying.length) + { + let carrying = entState.resourceCarrying[0]; + totalResourcesCarried[carrying.type] = (totalResourcesCarried[carrying.type] || 0) + carrying.amount; + } } Engine.GetGUIObjectByName("healthMultiple").hidden = averageHealth <= 0; if (averageHealth > 0) { let unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple"); let healthSize = unitHealthBar.size; healthSize.rtop = 100-100*Math.max(0, Math.min(1, averageHealth / maxHealth)); unitHealthBar.size = healthSize; Engine.GetGUIObjectByName("healthMultiple").tooltip = sprintf(translate("%(label)s %(current)s / %(max)s"), { "label": "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]", "current": Math.ceil(averageHealth), "max": Math.ceil(maxHealth) }); } Engine.GetGUIObjectByName("captureMultiple").hidden = maxCapturePoints <= 0; if (maxCapturePoints > 0) { let setCaptureBarPart = function(playerID, startSize) { let unitCaptureBar = Engine.GetGUIObjectByName("captureBarMultiple["+playerID+"]"); let sizeObj = unitCaptureBar.size; sizeObj.rtop = startSize; let size = 100*Math.max(0, Math.min(1, capturePoints[playerID] / maxCapturePoints)); sizeObj.rbottom = startSize + size; unitCaptureBar.size = sizeObj; unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[playerID].color, 128); unitCaptureBar.hidden = false; return startSize + size; }; let size = 0; for (let i in capturePoints) if (i != playerID) size = setCaptureBarPart(i, size); // last handle the owner's points, to keep those points on the bottom for clarity setCaptureBarPart(playerID, size); Engine.GetGUIObjectByName("captureMultiple").tooltip = sprintf(translate("%(label)s %(current)s / %(max)s"), { "label": "[font=\"sans-bold-13\"]" + translate("Capture points:") + "[/font]", "current": Math.ceil(capturePoints[playerID]), "max": Math.ceil(maxCapturePoints) }); } - Engine.GetGUIObjectByName("numberOfUnits").caption = selection.length; + let numberOfUnits = Engine.GetGUIObjectByName("numberOfUnits"); + numberOfUnits.caption = selection.length; + numberOfUnits.tooltip = Object.keys(totalResourcesCarried).map(res => + getCostComponentDisplayIcon(res) + totalResourcesCarried[res] + ).join(" "); // Unhide Details Area Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; } // Updates middle entity Selection Details Panel and left Unit Commands Panel function updateSelectionDetails() { let supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails"); let detailsPanel = Engine.GetGUIObjectByName("selectionDetails"); let commandsPanel = Engine.GetGUIObjectByName("unitCommands"); let selection = g_Selection.toList(); if (selection.length == 0) { Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; hideUnitCommands(); supplementalDetailsPanel.hidden = true; detailsPanel.hidden = true; commandsPanel.hidden = true; return; } /* If the unit has no data (e.g. it was killed), don't try displaying any data for it. (TODO: it should probably be removed from the selection too; also need to handle multi-unit selections) */ let entState = GetExtendedEntityState(selection[0]); if (!entState) return; // Fill out general info and display it if (selection.length == 1) displaySingle(entState); else displayMultiple(selection); // Show basic details. detailsPanel.hidden = false; // Fill out commands panel for specific unit selected (or first unit of primary group) updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection); // Show health bar for garrisoned units if the garrison panel is visible if (Engine.GetGUIObjectByName("unitGarrisonPanel") && !Engine.GetGUIObjectByName("unitGarrisonPanel").hidden) updateGarrisionHealthBar(entState, selection); } Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 18153) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 18154) @@ -1,1177 +1,1166 @@ /** * 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 * var data = { * "i": index * "item": item coming from the getItems function * "selection": list of currently selected items * "playerState": playerState * "unitEntState": first selected entity state * "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, addData is called, and can be used to abort the processing * of the current item by returning false. * It should return true if you want the panel to be filled. * * addData is used to add data to the data object on top * (or instead of) the standard data. * addData is not obligated, the function will just continue * with the content setters if no addData is present. * * After the addData, all functions starting with "set" are called. * These are used to set various parts of content. */ /* cache some formation info */ var g_availableFormations = new Map(); // available formations per player var g_formationsInfo = new Map(); var g_SelectionPanels = {}; // ALERT g_SelectionPanels.Alert = { "getMaxNumberOfItems": function() { return 2; }, "getItems": function(unitEntState) { if (!unitEntState.alertRaiser) return []; return ["increase", "end"]; }, "setAction": function(data) { data.button.onPress = function() { if (data.item == "increase") increaseAlertLevel(); else if (data.item == "end") endOfAlert(); }; }, "setTooltip": function(data) { if (data.item == "increase") { if (data.unitEntState.alertRaiser.hasRaisedAlert) data.button.tooltip = translate("Increase the alert level to protect more units"); else data.button.tooltip = translate("Raise an alert!"); } else if (data.item == "end") data.button.tooltip = translate("End of alert."); }, "setGraphics": function(data) { if (data.item == "increase") { data.button.hidden = !data.unitEntState.alertRaiser.canIncreaseLevel; if (data.unitEntState.alertRaiser.hasRaisedAlert) data.icon.sprite = "stretched:session/icons/bell_level2.png"; else data.icon.sprite = "stretched:session/icons/bell_level1.png"; } else if (data.item == "end") { data.button.hidden = !data.unitEntState.alertRaiser.hasRaisedAlert; data.icon.sprite = "stretched:session/icons/bell_level0.png"; } data.button.enabled = !data.button.hidden && controlsPlayer(data.unitEntState.player); } }; // BARTER g_SelectionPanels.Barter = { "getMaxNumberOfItems": function() { return 4; }, "rowLength": 4, "getItems": function(unitEntState, selection) { if (!unitEntState.barterMarket) return []; // ["food", "wood", "stone", "metal"] return BARTER_RESOURCES; }, "addData": function(data) { // data.item is the resource name in this case data.button = {}; data.icon = {}; data.amount = {}; for (var a of BARTER_ACTIONS) { data.button[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Button["+data.i+"]"); data.icon[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Icon["+data.i+"]"); data.amount[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Amount["+data.i+"]"); } data.selectionIcon = Engine.GetGUIObjectByName("unitBarterSellSelection["+data.i+"]"); data.amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL; if (Engine.HotkeyIsPressed("session.massbarter")) data.amountToSell *= BARTER_BUNCH_MULTIPLIER; data.isSelected = data.item == g_barterSell; return true; }, "setCountDisplay": function(data) { data.amount.Sell.caption = "-" + data.amountToSell; var sellPrice = data.unitEntState.barterMarket.prices.sell[g_barterSell]; var buyPrice = data.unitEntState.barterMarket.prices.buy[data.item]; data.amount.Buy.caption = "+" + Math.round(sellPrice / buyPrice * data.amountToSell); }, "setTooltip": function(data) { var resource = getLocalizedResourceName(data.item, "withinSentence"); data.button.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource }); data.button.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource }); }, "setAction": function(data) { data.button.Sell.onPress = function() { g_barterSell = data.item; }; var exchangeResourcesParameters = { "sell": g_barterSell, "buy": data.item, "amount": data.amountToSell }; data.button.Buy.onPress = function() { exchangeResources(exchangeResourcesParameters); }; }, "setGraphics": function(data) { var grayscale = data.isSelected ? "color: 0 0 0 100:grayscale:" : ""; // do we have enough of this resource to sell? var neededRes = {}; neededRes[data.item] = data.amountToSell; var canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", { "cost": neededRes, "player": data.unitEntState.player }) ? "color:255 0 0 80:" : ""; // Let's see if we have enough resources to barter. neededRes = {}; neededRes[g_barterSell] = data.amountToSell; var canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", { "cost": neededRes, "player": data.unitEntState.player }) ? "color:255 0 0 80:" : ""; data.icon.Sell.sprite = canSellCurrent + "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png"; data.icon.Buy.sprite = canBuyAny + "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png"; data.button.Buy.hidden = data.isSelected; data.button.Buy.enabled = controlsPlayer(data.unitEntState.player); data.button.Sell.hidden = false; data.selectionIcon.hidden = !data.isSelected; }, "setPosition": function(data) { setPanelObjectPosition(data.button.Sell, data.i, data.rowLength); setPanelObjectPosition(data.button.Buy, data.i + data.rowLength, data.rowLength); } }; // COMMAND g_SelectionPanels.Command = { "getMaxNumberOfItems": function() { return 6; }, "getItems": function(unitEntState) { let commands = []; for (let c in g_EntityCommands) { var info = g_EntityCommands[c].getInfo(unitEntState); if (!info) continue; info.name = c; commands.push(info); } return commands; }, "setTooltip": function(data) { data.button.tooltip = data.item.tooltip; }, "setAction": function(data) { data.button.onPress = function() { if (data.item.callback) data.item.callback(data.item); else performCommand(data.unitEntState.id, data.item.name); }; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.item.count || ""; }, "setGraphics": function(data) { data.button.enabled = controlsPlayer(data.unitEntState.player); let grayscale = data.button.enabled ? "" : "grayscale:"; data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon; }, "setPosition": function(data) { var size = data.button.size; // count on square buttons, so size.bottom is the width too var spacer = size.bottom + 1; // relative to the center ( = 50%) size.rleft = size.rright = 50; // offset from the center calculation size.left = (data.i - data.numberOfItems/2) * spacer; size.right = size.left + size.bottom; data.button.size = size; } }; //ALLY COMMAND g_SelectionPanels.AllyCommand = { "getMaxNumberOfItems": function() { return 2; }, "getItems": function(unitEntState) { var commands = []; for (var c in g_AllyEntityCommands) { var info = g_AllyEntityCommands[c].getInfo(unitEntState); if (!info) continue; info.name = c; commands.push(info); } return commands; }, "setTooltip": function(data) { data.button.tooltip = data.item.tooltip; }, "setAction": function(data) { data.button.onPress = function() { if (data.item.callback) data.item.callback(data.item); else performAllyCommand(data.unitEntState.id, data.item.name); }; }, "conflictsWith": ["Command"], "setCountDisplay": function(data) { data.countDisplay.caption = data.item.count || ""; }, "setGraphics": function(data) { data.button.enabled = data.item.count != undefined && data.item.count > 0; let grayscale = data.button.enabled ? "" : "grayscale:"; data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon; }, "setPosition": function(data) { var size = data.button.size; // count on square buttons, so size.bottom is the width too var spacer = size.bottom + 1; // relative to the center ( = 50%) size.rleft = size.rright = 50; // offset from the center calculation size.left = (data.i - data.numberOfItems/2) * spacer; size.right = size.left + size.bottom; data.button.size = size; } }; // CONSTRUCTION g_SelectionPanels.Construction = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function() { return getAllBuildableEntitiesFromSelection(); }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) // abort if no template return false; data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.template.requiredTechnology, "player": data.unitEntState.player }); if (data.template.cost) data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.template, 1), "player": data.unitEntState.player }); data.limits = getEntityLimitAndCount(data.playerState, data.entType); if (data.template.wallSet) data.template.auras = GetTemplateData(data.template.wallSet.templates.long).auras; return true; }, "setAction": function(data) { data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); }; }, "setTooltip": function(data) { var tooltip = getEntityNamesFormatted(data.template); tooltip += getVisibleEntityClassesFormatted(data.template); tooltip += getAurasTooltip(data.template); if (data.template.tooltip) tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(data.template); tooltip += getPopulationBonusTooltip(data.template); tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers); if (!data.technologyEnabled) tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { "technology": getEntityNames(GetTechnologyData(data.template.requiredTechnology)) }); if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); data.button.tooltip = tooltip; return true; }, "setGraphics": function(data) { var modifier = ""; if (!data.technologyEnabled || data.limits.canBeAddedCount == 0) { data.button.enabled = false; modifier += "color: 0 0 0 127:"; modifier += "grayscale:"; } else if (data.neededResources) { data.button.enabled = false; modifier += resourcesToAlphaMask(data.neededResources) +":"; } else data.button.enabled = controlsPlayer(data.unitEntState.player); if (data.template.icon) data.icon.sprite = modifier + "stretched:session/portraits/" + data.template.icon; }, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; // FORMATION g_SelectionPanels.Formation = { "getMaxNumberOfItems": function() { return 16; }, "rowLength": 4, "conflictsWith": ["Garrison"], "getItems": function(unitEntState) { if (!hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal")) return []; if (!g_availableFormations.has(unitEntState.player)) g_availableFormations.set(unitEntState.player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntState.player)); return g_availableFormations.get(unitEntState.player); }, "addData": function(data) { if (!g_formationsInfo.has(data.item)) g_formationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); data.formationInfo = g_formationsInfo.get(data.item); data.formationOk = canMoveSelectionIntoFormation(data.item); data.formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": data.selection, "formationTemplate": data.item }); return true; }, "setAction": function(data) { data.button.onPress = function() { performFormation(data.unitEntState.id, data.item); }; }, "setTooltip": function(data) { var tooltip = translate(data.formationInfo.name); if (!data.formationOk && data.formationInfo.tooltip) tooltip += "\n" + "[color=\"red\"]" + translate(data.formationInfo.tooltip) + "[/color]"; data.button.tooltip = tooltip; }, "setGraphics": function(data) { data.button.enabled = data.formationOk && controlsPlayer(data.unitEntState.player); var grayscale = data.formationOk ? "" : "grayscale:"; data.guiSelection.hidden = !data.formationSelected; data.icon.sprite = "stretched:"+grayscale+"session/icons/"+data.formationInfo.icon; } }; // GARRISON g_SelectionPanels.Garrison = { "getMaxNumberOfItems": function() { return 12; }, "rowLength": 4, "getItems": function(unitEntState, selection) { if (!unitEntState.garrisonHolder) return []; var groups = new EntityGroups(); for (var ent of selection) { var state = GetEntityState(ent); if (state.garrisonHolder) groups.add(state.garrisonHolder.entities); } return groups.getEntsGrouped(); }, "addData": function(data) { data.entType = data.item.template; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.name = getEntityNames(data.template); data.count = data.item.ents.length; return true; }, "setAction": function(data) { data.button.onPress = function() { unloadTemplate(data.item.template); }; }, "setTooltip": function(data) { var tooltip = sprintf(translate("Unload %(name)s"), { "name": data.name }) + "\n"; tooltip += translate("Single-click to unload 1. Shift-click to unload all of this type."); data.button.tooltip = tooltip; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.count || ""; }, "setGraphics": function(data) { var grayscale = ""; var ents = data.item.ents; var entplayer = GetEntityState(ents[0]).player; data.button.sprite = "color:" + rgbToGuiColor(g_Players[entplayer].color) +":"; if (!controlsPlayer(data.unitEntState.player) && !controlsPlayer(entplayer)) { data.button.enabled = false; grayscale = "grayscale:"; } data.icon.sprite = "stretched:" + grayscale + "session/portraits/" + data.template.icon; } }; // GATE g_SelectionPanels.Gate = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function(unitEntState, selection) { // Allow long wall pieces to be converted to gates var longWallTypes = {}; var walls = []; var gates = []; for (var ent of selection) { var state = GetEntityState(ent); if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template]) { var gateTemplate = getWallGateTemplate(state.id); if (gateTemplate) { var tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip; if (!tooltipString) { warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template"); tooltipString = ""; } walls.push({ "tooltip": translate(tooltipString), "template": gateTemplate, "callback": function (item) { transformWallToGate(item.template); } }); } // We only need one entity per type. longWallTypes[state.template] = true; } else if (state.gate && !gates.length) { gates.push({ "gate": state.gate, "tooltip": translate("Lock Gate"), "locked": true, "callback": function (item) { lockGate(item.locked); } }); gates.push({ "gate": state.gate, "tooltip": translate("Unlock Gate"), "locked": false, "callback": function (item) { lockGate(item.locked); } }); } // Show both 'locked' and 'unlocked' as active if the selected gates have both lock states. else if (state.gate && state.gate.locked != gates[0].gate.locked) for (var j = 0; j < gates.length; ++j) delete gates[j].gate.locked; } // Place wall conversion options after gate lock/unlock icons. var items = gates.concat(walls); return items; }, "setAction": function(data) { data.button.onPress = function() {data.item.callback(data.item); }; }, "setTooltip": function(data) { var tooltip = data.item.tooltip; if (data.item.template) { data.template = GetTemplateData(data.item.template); data.wallCount = data.selection.reduce(function (count, ent) { var state = GetEntityState(ent); if (hasClass(state, "LongWall") && !state.gate) ++count; return count; }, 0); tooltip += "\n" + getEntityCostTooltip(data.template, data.wallCount); data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.template, data.wallCount) }); if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); } data.button.tooltip = tooltip; }, "setGraphics": function(data) { data.button.enabled = controlsPlayer(data.unitEntState.player); var gateIcon; if (data.item.gate) { // If already a gate, show locking actions gateIcon = "icons/lock_" + GATE_ACTIONS[data.item.locked ? 0 : 1] + "ed.png"; if (data.item.gate.locked === undefined) data.guiSelection.hidden = false; else data.guiSelection.hidden = data.item.gate.locked != data.item.locked; } else { // otherwise show gate upgrade icon var template = GetTemplateData(data.item.template); if (!template) return; gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png"; data.guiSelection.hidden = true; } data.icon.sprite = (data.neededResources ? resourcesToAlphaMask(data.neededResources) + ":" : "") + "stretched:session/" + gateIcon; }, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; // PACK g_SelectionPanels.Pack = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function(unitEntState, selection) { var checks = {}; for (var ent of selection) { var state = GetEntityState(ent); if (!state.pack) continue; if (state.pack.progress == 0) { if (!state.pack.packed) checks.packButton = true; else if (state.pack.packed) checks.unpackButton = true; } else { // Already un/packing - show cancel button if (!state.pack.packed) checks.packCancelButton = true; else if (state.pack.packed) checks.unpackCancelButton = true; } } var 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; }, "setAction": function(data) { data.button.onPress = function() {data.item.callback(data.item); }; }, "setTooltip": function(data) { data.button.tooltip = data.item.tooltip; }, "setGraphics": function(data) { 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.unitEntState.player); }, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; // QUEUE g_SelectionPanels.Queue = { "getMaxNumberOfItems": function() { return 16; }, "getItems": function(unitEntState, selection) { return getTrainingQueueItems(selection); }, "resizePanel": function(numberOfItems, rowLength) { var numRows = Math.ceil(numberOfItems / rowLength); var panel = Engine.GetGUIObjectByName("unitQueuePanel"); var size = panel.size; var buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom; var margin = 4; size.top = size.bottom - numRows*buttonSize - (numRows+2)*margin; panel.size = size; }, "addData": function(data) { // differentiate between units and techs if (data.item.unitTemplate) { data.entType = data.item.unitTemplate; data.template = GetTemplateData(data.entType); } else if (data.item.technologyTemplate) { data.entType = data.item.technologyTemplate; data.template = GetTechnologyData(data.entType); } data.progress = Math.round(data.item.progress*100) + "%"; return data.template; }, "setAction": function(data) { data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, data.item.id); }; }, "setTooltip": function(data) { var tooltip = getEntityNames(data.template); if (data.item.neededSlots) { tooltip += "\n[color=\"red\"]" + translate("Insufficient population capacity:") + "\n[/color]"; tooltip += sprintf(translate("%(population)s %(neededSlots)s"), { - "population": getCostComponentDisplayName("population"), + "population": getCostComponentDisplayIcon("population"), "neededSlots": data.item.neededSlots }); } data.button.tooltip = tooltip; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.item.count > 1 ? data.item.count : ""; }, "setProgressDisplay": function(data) { // show the progress number for the first item if (data.i == 0) Engine.GetGUIObjectByName("queueProgress").caption = data.progress; var guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider["+data.i+"]"); var 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(data.item.progress * (size.right - size.left)); guiObject.size = size; }, "setGraphics": function(data) { if (data.template.icon) data.icon.sprite = "stretched:session/portraits/" + data.template.icon; data.button.enabled = controlsPlayer(data.unitEntState.player); } }; // RESEARCH g_SelectionPanels.Research = { "getMaxNumberOfItems": function() { return 8; }, "getItems": function(unitEntState, selection) { // TODO 8 is the row lenght, make variable if (getNumberOfRightPanelButtons() > 8 && selection.length > 1) return []; for (var ent of selection) { var entState = GetEntityState(ent); if (entState.production && entState.production.technologies.length) return entState.production.technologies; } return []; }, "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; }, "addData": function(data) { data.entType = data.item.pair ? [data.item.top, data.item.bottom] : [data.item]; data.template = data.entType.map(GetTechnologyData); // abort if no template found for any of the techs if (data.template.some(v => !v)) return false; // index one row below var shiftedIndex = data.i + data.rowLength; data.positions = data.item.pair ? [data.i, shiftedIndex] : [shiftedIndex]; data.positionsToHide = data.item.pair ? [] : [data.i]; // add top buttons to the data data.button = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchButton["+p+"]")); data.buttonsToHide = data.positionsToHide.map(p => Engine.GetGUIObjectByName("unitResearchButton["+p+"]")); data.icon = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchIcon["+p+"]")); data.unchosenIcon = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchUnchosenIcon["+p+"]")); data.neededResources = data.template.map(t => Engine.GuiInterfaceCall("GetNeededResources", { "cost": t.cost, "player": data.unitEntState.player })); data.requirementsPassed = data.entType.map(e => Engine.GuiInterfaceCall("CheckTechnologyRequirements", { "tech": e, "player": data.unitEntState.player })); data.pair = Engine.GetGUIObjectByName("unitResearchPair["+data.i+"]"); return true; }, "setTooltip": function(data) { for (var i in data.entType) { var tooltip = ""; var template = data.template[i]; tooltip = getEntityNamesFormatted(template); if (template.tooltip) tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(template); if (!data.requirementsPassed[i]) { tooltip += "\n" + template.requirementsTooltip; if (template.classRequirements) { var player = data.unitEntState.player; var current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0; var remaining = template.classRequirements.number - current; tooltip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), { "number": remaining }); } } if (data.neededResources[i]) tooltip += getNeededResourcesTooltip(data.neededResources[i]); data.button[i].tooltip = tooltip; } }, "setAction": function(data) { for (var i in data.entType) { // array containing the indices other buttons var others = Object.keys(data.template); others.splice(i, 1); var button = data.button[i]; // as we're in a loop, we need to limit the scope with a closure // else the last value of the loop will be taken, rather than the current one button.onpress = (function(template) { return function () { addResearchToQueue(data.unitEntState.id, template); }; })(data.entType[i]); // on mouse enter, show a cross over the other icons button.onmouseenter = (function(others, icons) { return function() { for (var j of others) icons[j].hidden = false; }; })(others, data.unchosenIcon); button.onmouseleave = (function(others, icons) { return function() { for (var j of others) icons[j].hidden = true; }; })(others, data.unchosenIcon); } }, "setGraphics": function(data) { for (var i in data.entType) { let button = data.button[i]; button.hidden = false; var modifier = ""; if (!data.requirementsPassed[i]) { button.enabled = false; modifier += "color: 0 0 0 127:"; modifier += "grayscale:"; } else if (data.neededResources[i]) { button.enabled = false; modifier += resourcesToAlphaMask(data.neededResources[i]) + ":"; } else button.enabled = controlsPlayer(data.unitEntState.player); if (data.template[i].icon) data.icon[i].sprite = modifier + "stretched:session/portraits/" + data.template[i].icon; } for (let button of data.buttonsToHide) button.hidden = true; // show the tech connector data.pair.hidden = data.item.pair == null; }, "setPosition": function(data) { for (var i in data.button) setPanelObjectPosition(data.button[i], data.positions[i], data.rowLength); setPanelObjectPosition(data.pair, data.i, data.rowLength); } }; // SELECTION g_SelectionPanels.Selection = { "getMaxNumberOfItems": function() { return 16; }, "rowLength": 4, "getItems": function(unitEntState, selection) { if (selection.length < 2) return []; return g_Selection.groups.getTemplateNames(); }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.name = getEntityNames(data.template); var ents = g_Selection.groups.getEntsByName(data.item); data.count = ents.length; for (var ent of ents) { var state = GetEntityState(ent); if (state.resourceCarrying && state.resourceCarrying.length !== 0) { if (!data.carried) data.carried = {}; var 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 = {}; var amount = state.trader.goods.amount; var type = state.trader.goods.type; var 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; } } return true; }, "setTooltip": function(data) { + let tooltip = data.name; if (data.carried) - { - var str = data.name + "\n"; - var ress = ["food", "wood", "stone", "metal"]; - for (var i = 0; i < 4; ++i) - { - if (data.carried[ress[i]]) - { - str += getCostComponentDisplayName(ress[i]) + data.carried[ress[i]]; - if (i !== 3) - str += " "; - } - } - data.button.tooltip = str; - } - else - data.button.tooltip = data.name; + tooltip += "\n" + Object.keys(data.carried).map(res => + getCostComponentDisplayIcon(res) + data.carried[res] + ).join(" "); + data.button.tooltip = tooltip; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.count || ""; }, "setAction": function(data) { data.button.onpressright = function() { changePrimarySelectionGroup(data.item, true); }; data.button.onpress = function() { changePrimarySelectionGroup(data.item, false); }; }, "setGraphics": function(data) { if (data.template.icon) data.icon.sprite = "stretched:session/portraits/" + data.template.icon; } }; // STANCE g_SelectionPanels.Stance = { "getMaxNumberOfItems": function() { return 5; }, "getItems": function(unitEntState) { if (!unitEntState.unitAI || !hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal")) return []; return unitEntState.unitAI.possibleStances; }, "addData": function(data) { data.stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", { "ents": data.selection, "stance": data.item }); return true; }, "setAction": function(data) { data.button.onPress = function() { performStance(data.unitEntState, data.item); }; }, "setTooltip": function(data) { data.button.tooltip = getStanceDisplayName(data.item) + "\n[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]"; }, "setGraphics": function(data) { data.guiSelection.hidden = !data.stanceSelected; data.icon.sprite = "stretched:session/icons/stances/"+data.item+".png"; data.button.enabled = controlsPlayer(data.unitEntState.player); } }; // TRAINING g_SelectionPanels.Training = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function() { return getAllTrainableEntitiesFromSelection(); }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.template.requiredTechnology, "player": data.unitEntState.player }); var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingBatchStatus(data.playerState, data.unitEntState.id, data.entType, data.selection); data.buildingsCountToTrainFullBatch = buildingsCountToTrainFullBatch; data.fullBatchSize = fullBatchSize; data.remainderBatch = remainderBatch; data.trainNum = buildingsCountToTrainFullBatch || 1; // train at least one unit if (Engine.HotkeyIsPressed("session.batchtrain")) data.trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; if (data.template.cost) data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.template, data.trainNum), "player": data.unitEntState.player }); return true; }, "setAction": function(data) { data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); }; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.trainNum > 1 ? data.trainNum : ""; }, "setTooltip": function(data) { var tooltip = ""; var key = Engine.ConfigDB_GetValue("user", "hotkey.session.queueunit." + (data.i + 1)); if (key) tooltip += "[color=\"255 251 131\"][font=\"sans-bold-16\"]\\[" + key + "][/font][/color] "; tooltip += getEntityNamesFormatted(data.template); tooltip += getVisibleEntityClassesFormatted(data.template); tooltip += getAurasTooltip(data.template); if (data.template.tooltip) tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(data.template, data.trainNum, data.unitEntState.id); data.limits = getEntityLimitAndCount(data.playerState, data.entType); tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") { if (data.template.health) tooltip += "\n[font=\"sans-bold-13\"]" + translate("Health:") + "[/font] " + data.template.health; if (data.template.attack) tooltip += "\n" + getAttackTooltip(data.template); if (data.template.armour) tooltip += "\n" + getArmorTooltip(data.template.armour); if (data.template.speed) tooltip += "\n" + getSpeedTooltip(data.template); } tooltip += "[color=\"255 251 131\"]" + formatBatchTrainingString(data.buildingsCountToTrainFullBatch, data.fullBatchSize, data.remainderBatch) + "[/color]"; if (!data.technologyEnabled) { var techName = getEntityNames(GetTechnologyData(data.template.requiredTechnology)); tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { "technology": techName }); } if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); data.button.tooltip = tooltip; }, // disable and enable buttons in the same way as when you do for the construction "setGraphics": g_SelectionPanels.Construction.setGraphics, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; /** * 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. */ var 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 "Training", "Construction", "Research", // normal together with training // UNIQUE PANES (importance doesn't matter) "Command", "AllyCommand", "Queue", "Selection", ]; Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/multiple_details_area.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/multiple_details_area.xml (revision 18153) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/multiple_details_area.xml (revision 18154) @@ -1,47 +1,47 @@ - + Hitpoints Capture points Index: ps/trunk/binaries/data/mods/public/gui/structree/draw.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/structree/draw.js (revision 18153) +++ ps/trunk/binaries/data/mods/public/gui/structree/draw.js (revision 18154) @@ -1,382 +1,382 @@ var g_DrawLimits = {}; // GUI limits. Populated by predraw() /** * Draw the structree * * (Actually resizes and changes visibility of elements, and populates text) */ function draw() { // Set basic state (positioning of elements mainly), but only once if (!Object.keys(g_DrawLimits).length) predraw(); let defWidth = 96; let defMargin = 4; let phaseList = g_ParsedData.phaseList; Engine.GetGUIObjectByName("civEmblem").sprite = "stretched:"+g_CivData[g_SelectedCiv].Emblem; Engine.GetGUIObjectByName("civName").caption = g_CivData[g_SelectedCiv].Name; Engine.GetGUIObjectByName("civHistory").caption = g_CivData[g_SelectedCiv].History; let i = 0; for (let pha of phaseList) { let s = 0; let y = 0; for (let stru of g_CivData[g_SelectedCiv].buildList[pha]) { let thisEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]"); if (thisEle === undefined) { error("\""+g_SelectedCiv+"\" has more structures in phase "+pha+" than can be supported by the current GUI layout"); break; } let c = 0; let rowCounts = []; stru = g_ParsedData.structures[stru]; Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_icon").sprite = "stretched:session/portraits/"+stru.icon; Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_icon").tooltip = assembleTooltip(stru); Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_name").caption = translate(stru.name.specific); thisEle.hidden = false; for (let r in g_DrawLimits[pha].prodQuant) { let p = 0; r = +r; // force int let prod_pha = phaseList[phaseList.indexOf(pha) + r]; if (stru.production.units[prod_pha]) { for (let prod of stru.production.units[prod_pha]) { prod = g_ParsedData.units[prod]; if (!drawProdIcon(i, s, r, p, prod)) break; p++; } } if (stru.wallset && prod_pha == pha) { for (let prod of [stru.wallset.gate, stru.wallset.tower]) { if (!drawProdIcon(i, s, r, p, prod)) break; p++; } } if (stru.production.technology[prod_pha]) { for (let prod of stru.production.technology[prod_pha]) { prod = (depath(prod).slice(0,5) == "phase") ? g_ParsedData.phases[prod] : g_ParsedData.techs[prod]; if (!drawProdIcon(i, s, r, p, prod)) break; p++; } } rowCounts[r] = p; if (p>c) c = p; hideRemaining("phase["+i+"]_struct["+s+"]_row["+r+"]_prod[", p, "]"); } let size = thisEle.size; size.left = y; size.right = size.left + ((c*24 < defWidth) ? defWidth : c*24) + 4; y = size.right + defMargin; thisEle.size = size; let eleWidth = size.right - size.left; let r; for (r in rowCounts) { let wid = rowCounts[r] * 24 - 4; let phaEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]"); size = phaEle.size; size.left = (eleWidth - wid)/2; phaEle.size = size; } ++r; hideRemaining("phase["+i+"]_struct["+s+"]_row[", r, "]"); ++s; } hideRemaining("phase["+i+"]_struct[", s, "]"); ++i; } let t = 0; for (let trainer of g_CivData[g_SelectedCiv].trainList) { let thisEle = Engine.GetGUIObjectByName("trainer["+t+"]"); if (thisEle === undefined) { error("\""+g_SelectedCiv+"\" has more unit trainers than can be supported by the current GUI layout"); break; } trainer = g_ParsedData.units[trainer]; Engine.GetGUIObjectByName("trainer["+t+"]_icon").sprite = "stretched:session/portraits/"+trainer.icon; Engine.GetGUIObjectByName("trainer["+t+"]_icon").tooltip = assembleTooltip(trainer); Engine.GetGUIObjectByName("trainer["+t+"]_name").caption = translate(trainer.name.specific); thisEle.hidden = false; let p = 0; for (let prod of trainer.trainer) { prod = g_ParsedData.units[prod]; if (!drawProdIcon(null, t, null, p, prod)) break; p++; } hideRemaining("trainer["+t+"]_prod[", p, "]"); let size = thisEle.size; size.right = size.left + ((p*24 < defWidth)?defWidth:p*24)+4; thisEle.size = size; let eleWidth = size.right - size.left; let wid = p * 24 - 4; let phaEle = Engine.GetGUIObjectByName("trainer["+t+"]_row"); size = phaEle.size; size.left = (eleWidth - wid)/2; phaEle.size = size; ++t; } hideRemaining("trainer[", t, "]"); let size = Engine.GetGUIObjectByName("display_tree").size; size.right = t > 0 ? -124 : -4; Engine.GetGUIObjectByName("display_tree").size = size; Engine.GetGUIObjectByName("display_trainers").hidden = t == 0; } function drawProdIcon(pha, s, r, p, prod) { let prodEle = Engine.GetGUIObjectByName("phase["+pha+"]_struct["+s+"]_row["+r+"]_prod["+p+"]"); if (pha === null) prodEle = Engine.GetGUIObjectByName("trainer["+s+"]_prod["+p+"]"); if (prodEle === undefined) { error("The "+(pha === null ? "trainer units" : "structures")+" of \""+g_SelectedCiv+"\" have more production icons than can be supported by the current GUI layout"); return false; } prodEle.sprite = "stretched:session/portraits/"+prod.icon; prodEle.tooltip = assembleTooltip(prod); prodEle.hidden = false; return true; } /** * Calculate row position offset (accounting for different number of prod rows per phase). */ function getPositionOffset(idx) { let phases = g_ParsedData.phaseList.length; let size = 92*idx; // text, image and offset size += 24 * (phases*idx - (idx-1)*idx/2); // phase rows (phase-currphase+1 per row) return size; } function hideRemaining(prefix, idx, suffix) { let obj = Engine.GetGUIObjectByName(prefix+idx+suffix); while (obj) { obj.hidden = true; ++idx; obj = Engine.GetGUIObjectByName(prefix+idx+suffix); } } /** * Positions certain elements that only need to be positioned once * (as does not reposition automatically). * * Also detects limits on what the GUI can display by iterating through the set * elements of the GUI. These limits are then used by draw(). */ function predraw() { let phaseList = g_ParsedData.phaseList; let initIconSize = Engine.GetGUIObjectByName("phase[0]_struct[0]_row[0]_prod[0]").size; let phaseCount = phaseList.length; let i = 0; for (let pha of phaseList) { let offset = getPositionOffset(i); // Align the phase row Engine.GetGUIObjectByName("phase["+i+"]").size = "8 16+"+offset+" 100% 100%"; // Set phase icon let phaseIcon = Engine.GetGUIObjectByName("phase["+i+"]_phase"); phaseIcon.sprite = "stretched:session/portraits/"+g_ParsedData.phases[pha].icon; phaseIcon.size = "16 32+"+offset+" 48+16 48+32+"+offset; // Position prod bars let j = 1; for (; j < phaseCount - i; ++j) { let prodBar = Engine.GetGUIObjectByName("phase["+i+"]_bar["+(j-1)+"]"); prodBar.size = "40 1+"+(24*j)+"+98+"+offset+" 100%-8 1+"+(24*j)+"+98+"+offset+"+22"; // Set phase icon let prodBarIcon = Engine.GetGUIObjectByName("phase["+i+"]_bar["+(j-1)+"]_icon"); prodBarIcon.sprite = "stretched:session/portraits/"+g_ParsedData.phases[phaseList[i+j]].icon; } // Hide remaining prod bars hideRemaining("phase["+i+"]_bar[", j-1, "]"); let s = 0; let ele = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]"); g_DrawLimits[pha] = { "structQuant": 0, "prodQuant": [] }; do { // Position production icons for (let r in phaseList.slice(phaseList.indexOf(pha))) { let p=1; let prodEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]_prod["+p+"]"); do { let prodsize = prodEle.size; prodsize.left = (initIconSize.right+4) * p; prodsize.right = (initIconSize.right+4) * (p+1) - 4; prodEle.size = prodsize; p++; prodEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]_prod["+p+"]"); } while (prodEle !== undefined); // Set quantity of productions in this row g_DrawLimits[pha].prodQuant[r] = p; // Position the prod row Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]").size = "4 100%-"+24*(phaseCount - i - r)+" 100%-4 100%"; } // Hide unused struct rows for (let j = phaseCount - i; j < phaseCount; ++j) Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+j+"]").hidden = true; let size = ele.size; size.bottom += Object.keys(g_DrawLimits[pha].prodQuant).length*24; ele.size = size; s++; ele = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]"); } while (ele !== undefined); // Set quantity of structures in each phase g_DrawLimits[pha].structQuant = s; ++i; } hideRemaining("phase[", i, "]"); hideRemaining("phase[", i, "]_bar"); let t = 0; let ele = Engine.GetGUIObjectByName("trainer["+t+"]"); g_DrawLimits.trainer = { "trainerQuant": 0, "prodQuant": 0 }; let x = 4; do { let p = 0; let prodEle = Engine.GetGUIObjectByName("trainer["+t+"]_prod["+p+"]"); do { let prodsize = prodEle.size; prodsize.left = (initIconSize.right+4) * p; prodsize.right = (initIconSize.right+4) * (p+1) - 4; prodEle.size = prodsize; p++; prodEle = Engine.GetGUIObjectByName("trainer["+t+"]_prod["+p+"]"); } while (prodEle !== undefined); Engine.GetGUIObjectByName("trainer["+t+"]_row").size = "4 100%-24"+" 100%-4 100%"; g_DrawLimits.trainer.prodQuant = p; let size = ele.size; size.top += x; size.bottom += x + 24; x += size.bottom - size.top + 8; ele.size = size; t++; ele = Engine.GetGUIObjectByName("trainer["+t+"]"); } while (ele !== undefined); g_DrawLimits.trainer.trainerQuant = t; } /** * Assemble a tooltip text * * @param template Information about a Unit, a Structure or a Technology * * @return The tooltip text, formatted. */ function assembleTooltip(template) { let txt = getEntityNamesFormatted(template); txt += '\n' + getEntityCostTooltip(template, 1); if (template.tooltip) txt += '\n' + g_TooltipTextFormats.body[0] + translate(template.tooltip) + g_TooltipTextFormats.body[1]; if (template.auras) txt += getAurasTooltip(template); if (template.health) txt += '\n' + sprintf(translate("%(label)s %(details)s"), { "label": g_TooltipTextFormats.header[0] + translate("Health:") + g_TooltipTextFormats.header[1], "details": template.health }); if (template.healer) txt += '\n' + getHealerTooltip(template); if (template.attack) txt += '\n' + getAttackTooltip(template); if (template.armour) txt += '\n' + getArmorTooltip(template.armour); if (template.speed) txt += '\n' + getSpeedTooltip(template); if (template.gather) { let rates = []; for (let type in template.gather) rates.push(sprintf(translate("%(resourceIcon)s %(rate)s"), { - "resourceIcon": getCostComponentDisplayName(type), + "resourceIcon": getCostComponentDisplayIcon(type), "rate": template.gather[type] })); txt += '\n' + sprintf(translate("%(label)s %(details)s"), { "label": g_TooltipTextFormats.header[0] + translate("Gather Rates:") + g_TooltipTextFormats.header[1], "details": rates.join(" ") }); } txt += getPopulationBonusTooltip(template); return txt; }