From fb38c8781b795ec84667f63d61ad634761b7bdda Mon Sep 17 00:00:00 2001 From: Vantha Date: Wed, 24 Sep 2025 16:52:59 +0200 Subject: [PATCH] More versatile placement of research buttons This patch allows techs to optionally define a property "placeBelow" specifying a combination of class requirements. The selection panel then tries to place their research button below the training button whose unit matches these classes -- if there is only and exactly one. This is useful for techs only affecting a single type of unit: it e.g. allows always placing the fishing nets research button below the fishing boats training button. To prevent duplicating information in "placeBelow" (e.g. from "affects"), it can also be set to two magic values: "{AffectedUnit}" and "{UnlockedUnit}". In practice, many techs use those, but class combination remain useful for granular control in very specific cases. Regarding tech pairs, as explained in a comment, their two research buttons can now be placed in three different arrangements (with descending preference): 1. Vertically below a single training button. 2. Horizontally below two adjacent training buttons. 3. Horizontally adjacent in the bottom row (below no training buttons). Vertically in the third and fourth rows (below no training button) -- how it was previously -- is no longer done as it'd conflict with and doesn't fit the new layout and the clear separation between "generic" and "specific" techs. Whenever a research button can't be placed in their preferred location because it is already occupied, the code simply falls back to the default position in the bottom row. This patch also adds a new game option to hide the new small arrows above the research buttons in order to give more experienced players who already know the techs well the possibility to reduce visual clutter. By default, the arrows are shown. --- binaries/data/config/default.cfg | 1 + ...ntal_pair.png => horizontal_tech_pair.png} | 0 .../icons/horizontal_tech_pair_disabled.png | 3 + .../ui/session/icons/tech_affects.png | 3 + .../session/icons/tech_affects_disabled.png | 3 + ...rtical_pair.png => vertical_tech_pair.png} | 0 .../icons/vertical_tech_pair_disabled.png | 3 + .../art/textures/ui/session/ui_border.png | 4 +- .../mods/public/globalscripts/Templates.js | 2 + .../data/mods/public/gui/options/options.json | 6 + .../public/gui/session/selection_panels.js | 364 ++++++++++++++---- .../selection_panels_right/research_panel.xml | 12 +- .../data/mods/public/gui/session/session.xml | 2 +- .../data/mods/public/gui/session/sprites.xml | 5 + .../mods/public/gui/session/unit_commands.js | 32 +- .../simulation/data/technologies/agoge.json | 1 + .../data/technologies/celtic_auxiliaries.json | 1 + .../data/technologies/civil_service_01.json | 1 + .../data/technologies/civil_service_02.json | 1 + .../data/technologies/cost_healer.json | 1 + .../data/technologies/crossbow_training.json | 1 + .../fishing_boat_gather_capacity.json | 1 + .../fishing_boat_gather_rate.json | 1 + .../data/technologies/grove_of_fetters.json | 1 + .../data/technologies/heal_range.json | 1 + .../data/technologies/heal_range_2.json | 1 + .../data/technologies/heal_rate.json | 1 + .../data/technologies/heal_rate_2.json | 1 + .../technologies/health_civilians_01.json | 1 + .../data/technologies/helot_economy.json | 1 + .../data/technologies/hoplite_tradition.json | 1 + .../data/technologies/immortals.json | 1 + .../data/technologies/krypteia.json | 1 + .../data/technologies/nisean_horses.json | 1 + .../data/technologies/reformed_army_sele.json | 1 + .../technologies/siege_bolt_accuracy.json | 1 + .../data/technologies/silvershields.json | 1 + .../data/technologies/trader_health.json | 1 + .../technologies/traditional_army_sele.json | 1 + .../data/technologies/tyrtean_paeans.json | 1 + .../technologies/unlock_champion_cavalry.json | 1 + .../unlock_champion_chariots.json | 1 + .../unlock_champion_infantry.json | 1 + .../unlock_civilians_house_generic.json | 1 + .../unlock_civilians_house_kush.json | 1 + .../technologies/unlock_females_house.json | 1 + .../data/technologies/unlock_neodamodes.json | 1 + .../data/technologies/wagon_trains.json | 1 + .../technologies/warship_arrow_attack.json | 1 + .../technologies/warship_fireship_attack.json | 1 + .../technologies/warship_ramming_attack.json | 1 + .../technologies/warship_siege_attack.json | 1 + 52 files changed, 379 insertions(+), 98 deletions(-) rename binaries/data/mods/public/art/textures/ui/session/icons/{horizontal_pair.png => horizontal_tech_pair.png} (100%) create mode 100644 binaries/data/mods/public/art/textures/ui/session/icons/horizontal_tech_pair_disabled.png create mode 100644 binaries/data/mods/public/art/textures/ui/session/icons/tech_affects.png create mode 100644 binaries/data/mods/public/art/textures/ui/session/icons/tech_affects_disabled.png rename binaries/data/mods/public/art/textures/ui/session/icons/{vertical_pair.png => vertical_tech_pair.png} (100%) create mode 100644 binaries/data/mods/public/art/textures/ui/session/icons/vertical_tech_pair_disabled.png diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index 590c5b549c..21c30a0531 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -516,6 +516,7 @@ defaultformation = "special/formations/box" ; For walking orders, automatically formationwalkonly = "true" ; Formations are disabled when giving gather/attack/... orders. howtoshownames = 0 ; Whether the specific names are show as default, as opposed to the generic names. And whether the secondary names are shown. (0 - show both; specific names primary, 1 - show both; generic names primary, 2 - show only specific names, 3 - show only generic names) selectformationasone = "true" ; Whether to select formations as a whole by default. +techarrows = true ; Whether to show an arrow above some techs that indicates which unit they affect or unlock. [gui.session.minimap] ; Icons that are displayed for some entities on a minimap. diff --git a/binaries/data/mods/public/art/textures/ui/session/icons/horizontal_pair.png b/binaries/data/mods/public/art/textures/ui/session/icons/horizontal_tech_pair.png similarity index 100% rename from binaries/data/mods/public/art/textures/ui/session/icons/horizontal_pair.png rename to binaries/data/mods/public/art/textures/ui/session/icons/horizontal_tech_pair.png diff --git a/binaries/data/mods/public/art/textures/ui/session/icons/horizontal_tech_pair_disabled.png b/binaries/data/mods/public/art/textures/ui/session/icons/horizontal_tech_pair_disabled.png new file mode 100644 index 0000000000..fcbea4b1ce --- /dev/null +++ b/binaries/data/mods/public/art/textures/ui/session/icons/horizontal_tech_pair_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0af0db2f9dbdd451452ef7968fef7c73a4d91faf8b9d6663a3f01972ae5364c6 +size 1588 diff --git a/binaries/data/mods/public/art/textures/ui/session/icons/tech_affects.png b/binaries/data/mods/public/art/textures/ui/session/icons/tech_affects.png new file mode 100644 index 0000000000..6b04f11542 --- /dev/null +++ b/binaries/data/mods/public/art/textures/ui/session/icons/tech_affects.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc488cbc99d9c9a16b287cc9fb39fc0e2a25f2046f30a3b39ae6855ae90883bf +size 798 diff --git a/binaries/data/mods/public/art/textures/ui/session/icons/tech_affects_disabled.png b/binaries/data/mods/public/art/textures/ui/session/icons/tech_affects_disabled.png new file mode 100644 index 0000000000..73a3f23279 --- /dev/null +++ b/binaries/data/mods/public/art/textures/ui/session/icons/tech_affects_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79fcc1b450dc61d7b1df551785a8a116b033e54e41d256adf95c05f66cb1c07e +size 798 diff --git a/binaries/data/mods/public/art/textures/ui/session/icons/vertical_pair.png b/binaries/data/mods/public/art/textures/ui/session/icons/vertical_tech_pair.png similarity index 100% rename from binaries/data/mods/public/art/textures/ui/session/icons/vertical_pair.png rename to binaries/data/mods/public/art/textures/ui/session/icons/vertical_tech_pair.png diff --git a/binaries/data/mods/public/art/textures/ui/session/icons/vertical_tech_pair_disabled.png b/binaries/data/mods/public/art/textures/ui/session/icons/vertical_tech_pair_disabled.png new file mode 100644 index 0000000000..b39d13283b --- /dev/null +++ b/binaries/data/mods/public/art/textures/ui/session/icons/vertical_tech_pair_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d715163e253b20dfeeb4a9509b0a0057d607b27536597e1d3442a939f9c2ee83 +size 1649 diff --git a/binaries/data/mods/public/art/textures/ui/session/ui_border.png b/binaries/data/mods/public/art/textures/ui/session/ui_border.png index 5015b95617..cff5065fe0 100644 --- a/binaries/data/mods/public/art/textures/ui/session/ui_border.png +++ b/binaries/data/mods/public/art/textures/ui/session/ui_border.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeb1f48c5cc648e832e42f474166f3ba6bb052c724d1066ee828d6c8edce0371 -size 6361 +oid sha256:11eb2025f1b1db59b1cfe0ed35eea70d0cb0b48eba8f704a09eea2b0be0ca4dc +size 6650 diff --git a/binaries/data/mods/public/globalscripts/Templates.js b/binaries/data/mods/public/globalscripts/Templates.js index 1a781fe547..aa8452e71d 100644 --- a/binaries/data/mods/public/globalscripts/Templates.js +++ b/binaries/data/mods/public/globalscripts/Templates.js @@ -592,6 +592,8 @@ function GetTechnologyDataHelper(template, civ, resources) ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; + if (template.placeBelow) + ret.placeBelow = template.placeBelow; return ret; } diff --git a/binaries/data/mods/public/gui/options/options.json b/binaries/data/mods/public/gui/options/options.json index eff6445cc0..3f0e02d47f 100644 --- a/binaries/data/mods/public/gui/options/options.json +++ b/binaries/data/mods/public/gui/options/options.json @@ -640,6 +640,12 @@ "tooltip": "Show detailed tooltips for trainable units in structures.", "config": "showdetailedtooltips" }, + { + "type": "boolean", + "label": "Tech arrows", + "tooltip": "Show an arrow above specific techs that points to the unit that they affect or unlock.", + "config": "gui.session.techarrows" + }, { "type": "dropdown", "label": "Naming of entities", diff --git a/binaries/data/mods/public/gui/session/selection_panels.js b/binaries/data/mods/public/gui/session/selection_panels.js index 3134c54736..8ffd6a8402 100644 --- a/binaries/data/mods/public/gui/session/selection_panels.js +++ b/binaries/data/mods/public/gui/session/selection_panels.js @@ -647,8 +647,30 @@ g_SelectionPanels.Research = { return 10; }, "rowLength": 10, + "init": function() + { + const updateAffectsIconVisibility = () => + { + this.helper.showAffectsIcons = Engine.ConfigDB_GetValue("user", "gui.session.techarrows") === "true"; + }; + registerConfigChangeHandler(changes => + { + if (changes.has("gui.session.techarrows")) + updateAffectsIconVisibility(); + // They will be rerendered with the new visibility next frame. + }); + updateAffectsIconVisibility(); + }, + "reset": function() + { + this.helper.occupiedPositions = new Set(); + this.helper.bottomRowButtonCount = 0; + }, "getItems": function(unitEntStates) { + if (getNumberOfRightPanelButtons() >= this.rowLength * 2) + return []; + let ret = []; if (unitEntStates.length == 1) { @@ -690,8 +712,7 @@ g_SelectionPanels.Research = { k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k]) )); - if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() && - getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2)) + if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems()) ret = ret.concat(filteredTechs.map(tech => ({ "tech": tech, "techCostMultiplier": state.researcher.techCostMultiplier, @@ -704,77 +725,278 @@ g_SelectionPanels.Research = { "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; + // Remove the button it would have been paired with as well. + Engine.GetGUIObjectByName("unitResearchButton[" + (i + this.getMaxNumberOfItems()) + "]").hidden = true; }, "setupButton": function(data) { if (!data.item.tech) { - g_SelectionPanels.Research.hideItem(data.i, data.rowLength); + this.hideItem(data.i, data.rowLength); return false; } - // Start position (start at the bottom) - let position = data.i + data.rowLength; + // There are twice as many button objects than this.getMaxNumberOfItems() + // This is because each item could be a tech pair and need a second one in addition to the one at data.i + data.j = data.i + this.getMaxNumberOfItems(); - // Only show the top button for pairs - if (!data.item.tech.pair) - Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; + const playerState = GetSimState().players[data.player]; - // Set up the tech connector - const 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) - const player = data.player; - const playerState = GetSimState().players[player]; - for (const tech of (data.item.tech.pair || [data.item.tech])) + if (data.item.tech.pair) { - // Don't change the object returned by GetTechnologyData - const template = clone(GetTechnologyData(tech, playerState.civ)); - if (!template) - return false; + const firstTemplate = GetTechnologyData(data.item.tech.pair[0], playerState.civ); + const secondTemplate = GetTechnologyData(data.item.tech.pair[1], playerState.civ); - // Not allowed by civ. - if (!template.reqs) + // template.reqs is false if the tech isn't researchable by the current civ. + const firstResearchable = !!firstTemplate?.reqs; + const secondResearchable = !!secondTemplate?.reqs; + + if (firstResearchable && secondResearchable) + // Ideal/expected case: Display both techs in a pair. + return this.helper.setupButtonPair(data, data.item.tech.pair[0], data.item.tech.pair[1], firstTemplate, + secondTemplate, playerState); + + // At least one of the two is not valid or researchable. If the other one is, display it as a single tech + // on its own. + if (firstResearchable && !secondResearchable) + return this.helper.setupSingleButton(data, data.item.tech.pair[0], firstTemplate, playerState); + if (!firstResearchable && secondResearchable) + return this.helper.setupSingleButton(data, data.item.tech.pair[1], secondTemplate, playerState); + + // Neither of the two are valid and researchable. + this.hideItem(data.i, data.rowLength); + return false; + } + + const template = GetTechnologyData(data.item.tech, playerState.civ); + // template.reqs is false if the tech isn't researchable by the current civ. + if (template?.reqs) + return this.helper.setupSingleButton(data, data.item.tech, template, playerState); + + this.hideItem(data.i, data.rowLength); + return false; + }, + "helper": { + // Techs can optionally define a placeBelow property that specifies a unit whose training button they want to be placed below. + // It can be: + // - "{UnlockedUnit}": the first unit whose requirements (of the Identity component) contain the tech. + // - "{AffectedUnit}": the first unit whose stats are modified (receives buffs or debuffs) by the tech. + // - class combination: the first unit whose identity classes match that combination. + "findTargetTrainingButton": function(data, techName, template) + { + // Also check whether the other right panel buttons (training, constructing, upgrading) reach the second row. + // In that case, we want to place all techs in the bottom row. Research buttons should never be placed in the + // same row as these. + if (!template.placeBelow || getNumberOfRightPanelButtons() > data.rowLength) + return -1; + + const indices = []; + if (template.placeBelow === "{UnlockedUnit}") { - // One of the pair may still be researchable by the current civ, - // hence don't hide everything. - Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; - pair.hidden = true; - continue; + getAllTrainableEntitiesFromSelection().forEach((trainableTemplate, i) => + { + if (GetTemplateData(trainableTemplate, data.player)?.requirements?.Techs?._string.split(/\s+/).includes(techName)) + indices.push(i); + }); + } + else + { + let targetClassList; + if (template.placeBelow === "{AffectedUnit}") + { + const affectsList = (template.affects || []); + for (const mod of template.modifications) + if (mod.affects) + affectsList.push(mod.affects); + + targetClassList = affectsList.map(classes => classes.split(/\s+/)); + } + else + targetClassList = [template.placeBelow.split(/\s+/)]; + + getAllTrainableEntitiesFromSelection().forEach((trainableTemplate, i) => + { + if (MatchesClassList(GetTemplateData(trainableTemplate, data.player).visibleIdentityClasses, targetClassList)) + indices.push(i); + }); + } + // Only choose a training button if it's the only matching one. + if (indices.length !== 1) + return -1; + + // Make sure to account for the other buttons placed before the unit training ones. + return indices[0] + ["Construction", "Pack", "Gate", "Upgrade"].reduce((total, panel) => + total + g_unitPanelButtons[panel], 0 + ); + + }, + "setupSingleButton": function(data, techName, template, playerState) + { + // The item is not a tech pair. So hide the button that data.button would have been paired with. + Engine.GetGUIObjectByName("unitResearchButton[" + data.j + "]").hidden = true; + + // Note: The GUI object container of the research buttons (unlike the one of the training buttons) only reaches up to the second row. + // This means that, for example, a research button with position 5 is located directly one row under a training button with position 5. + let position = this.findTargetTrainingButton(data, techName, template); + + let placeInBottomRow = position == -1; + if (!placeInBottomRow && this.occupiedPositions.has(position)) + { + // Try to fall back to the third (second-to-bottom) row. + position += data.rowLength; + if (this.occupiedPositions.has(position)) + // Both positions below the target unit are already used by other techs. + // Note: Ideally this should never occur. Two techs per unit should be the limit. This here is just edge case handling. + placeInBottomRow = true; + } + if (placeInBottomRow) + { + // Try to move it to the fourth (bottom) row. + if (this.bottomRowButtonCount >= data.rowLength) + return false; // Bottom row is full, we can't display it. + position = this.bottomRowButtonCount + data.rowLength * 2; } - for (const res in template.cost) - template.cost[res] *= data.item.techCostMultiplier[res] !== undefined ? data.item.techCostMultiplier[res] : 1; + Engine.GetGUIObjectByName("unitResearchVerticalPairIcon[" + data.i + "]").hidden = true; + Engine.GetGUIObjectByName("unitResearchHorizontalPairIcon[" + data.i + "]").hidden = true; + + // When it's not "active", it's grayed out. + const buttonActive = this.buildButton(data, techName, template, position, playerState, data.button, data.icon); + this.buildAffectsIcon(data.i, !placeInBottomRow, buttonActive); + + return true; + }, + "setupButtonPair": function(data, firstTechName, secondTechName, firstTemplate, secondTemplate, playerState) + { + // Note: The GUI object container of the research buttons (unlike the one of the training buttons) only + // reaches up to the second row. This means that, for example, a research button with position 5 is located + // directly one row under a training button with position 5. + let firstPosition = this.findTargetTrainingButton(data, firstTechName, firstTemplate); + let secondPosition = this.findTargetTrainingButton(data, secondTechName, secondTemplate); + + // Possible placements of tech pair with descending preference: + // - Vertically below a single unit. + // - Horizontally below two adjacent units. + // - Horizontally adjacent below no unit in the bottom row. + + // Only ever place either below a unit, if the other can be too and below the same or an adjacent one. + let placeInBottomRow = firstPosition == -1 || secondPosition == -1 || Math.abs(firstPosition - secondPosition) > 1; + let placeHorizontally = true; + if (!placeInBottomRow && firstPosition === secondPosition) + { + // Both want to be placed under the same unit. + // Try to place the pair vertically by moving the second one down to the third (second-to-bottom) row, + // below the first one. + secondPosition += data.rowLength; + if (this.occupiedPositions.has(firstPosition) || this.occupiedPositions.has(secondPosition)) + placeInBottomRow = true; + else + placeHorizontally = false; + } + else if (!placeInBottomRow && (this.occupiedPositions.has(firstPosition) || this.occupiedPositions.has(secondPosition))) + { + // At least one of the two respective positions in the second (third-to-bottom) row is occupied. + // So try move both to the third. + firstPosition += data.rowLength; + secondPosition += data.rowLength; + if (this.occupiedPositions.has(firstPosition) || this.occupiedPositions.has(secondPosition)) + // Neither the two positions in the second row nor the third row below the target training buttons + // are available. + placeInBottomRow = true; + } + + if (placeInBottomRow) + { + // Try to move both to the bottom row. + if (this.bottomRowButtonCount >= data.rowLength - 1) + // Not enough space in the bottom row for both of them. We can't display them. + return false; + + firstPosition = this.bottomRowButtonCount + data.rowLength * 2; + secondPosition = firstPosition + 1; + } + + // Note: the button indices here aren't related to positioning at all. + const firstButtonIndex = data.i; + const secondButtonIndex = data.j; + const firstButton = data.button; + const secondButton = Engine.GetGUIObjectByName("unitResearchButton[" + secondButtonIndex + "]"); + const firstIcon = data.icon; + const secondIcon = Engine.GetGUIObjectByName("unitResearchIcon[" + secondButtonIndex + "]"); + + // When it's not "active", it's grayed out. + const firstButtonActive = this.buildButton(data, firstTechName, firstTemplate, firstPosition, playerState, + firstButton, firstIcon); + this.buildAffectsIcon(firstButtonIndex, !placeInBottomRow, firstButtonActive); + + // When it's not "active", it's grayed out. + const secondButtonActive = this.buildButton(data, secondTechName, secondTemplate, secondPosition, playerState, + secondButton, secondIcon); + this.buildAffectsIcon(secondButtonIndex, !placeInBottomRow && placeHorizontally, secondButtonActive); + + this.buildPairIcon(false, firstButtonIndex, placeHorizontally && secondPosition > firstPosition, firstButtonActive); + this.buildPairIcon(false, secondButtonIndex, placeHorizontally && secondPosition < firstPosition, secondButtonActive); + this.buildPairIcon(true, firstButtonIndex, !placeHorizontally, firstButtonActive); + this.buildPairIcon(true, secondButtonIndex, false, secondButtonActive); + + // While hovering over either button, show a cross over the other one. + // TODO: The following lines have to be executed only once, technically, and not every this function is called. + const firstUnchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + firstButtonIndex + "]"); + const secondUnchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + secondButtonIndex + "]"); + firstButton.onMouseEnter = () => { secondUnchosenIcon.hidden = false; }; + firstButton.onMouseLeave = () => { secondUnchosenIcon.hidden = true; }; + secondButton.onMouseEnter = () => { firstUnchosenIcon.hidden = false; }; + secondButton.onMouseLeave = () => { firstUnchosenIcon.hidden = true; }; + + return true; + }, + "buildAffectsIcon": function(i, show, enable) + { + const icon = Engine.GetGUIObjectByName("unitResearchAffectsIcon[" + i + "]"); + const hidden = !show || !this.showAffectsIcons; + icon.hidden = hidden; + if (!hidden) + icon.sprite = "stretched:session/icons/" + (enable ? "tech_affects.png" : "tech_affects_disabled.png"); + }, + "buildPairIcon": function(vertical, i, show, enable) + { + const icon = Engine.GetGUIObjectByName("unitResearch" + (vertical ? "Vertical" : "Horizontal") + "PairIcon[" + i + "]"); + icon.hidden = !show; + if (show) + icon.sprite = "stretched:session/icons/" + + (vertical ? + enable ? "vertical_tech_pair.png" : "vertical_tech_pair_disabled.png" : + enable ? "horizontal_tech_pair.png" : "horizontal_tech_pair_disabled.png"); + }, + "buildButton": function(baseData, techName, template, position, playerState, button, icon) + { + // Make sure to not modify the original template. + const adaptedTemplate = clone(template); + for (const res in adaptedTemplate.cost) + adaptedTemplate.cost[res] *= + baseData.item.techCostMultiplier[res] !== undefined ? baseData.item.techCostMultiplier[res] : 1; const neededResources = Engine.GuiInterfaceCall("GetNeededResources", { - "cost": template.cost, - "player": player + "cost": adaptedTemplate.cost, + "player": baseData.player }); const requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", { - "tech": tech, - "player": player + "tech": techName, + "player": baseData.player }); - const button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]"); - const icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]"); - const tooltips = [ getEntityNamesFormatted, getEntityTooltip, getEntityCostTooltip, getTemplateViewerOnRightClickTooltip - ].map(func => func(template)); + ].map(func => func(adaptedTemplate)); if (!requirementsPassed) { - let tip = template.requirementsTooltip; - const reqs = template.reqs; + let tip = adaptedTemplate.requirementsTooltip; + const reqs = adaptedTemplate.reqs; for (const req of reqs) { if (!req.entities) @@ -819,39 +1041,27 @@ g_SelectionPanels.Research = { button.onPress = (t => function() { - addResearchToQueue(data.item.researchFacilityId, t); - })(tech); + addResearchToQueue(baseData.item.researchFacilityId, t); + })(techName); const showTemplateFunc = (t => function() { showTemplateDetails( t, - GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv); + GetTemplateData(baseData.unitEntStates.find(state => state.id == baseData.item.researchFacilityId).template).nativeCiv); }); - button.onPressRight = showTemplateFunc(tech); - button.onPressRightDisabled = showTemplateFunc(tech); - - if (data.item.tech.pair) - { - // On mouse enter, show a cross over the other icon - const unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]"); - button.onMouseEnter = function() - { - unchosenIcon.hidden = false; - }; - button.onMouseLeave = function() - { - unchosenIcon.hidden = true; - }; - } + button.onPressRight = showTemplateFunc(techName); + button.onPressRightDisabled = showTemplateFunc(techName); button.hidden = false; let modifier = ""; + let isActive = true; if (!requirementsPassed) { button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; + isActive = false; } else if (neededResources) { @@ -859,26 +1069,32 @@ g_SelectionPanels.Research = { modifier += resourcesToAlphaMask(neededResources) + ":"; } else - button.enabled = controlsPlayer(data.player); + button.enabled = controlsPlayer(baseData.player); - if (data.item.isUpgrading) + if (baseData.item.isUpgrading) { button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; + isActive = false; button.tooltip += "\n" + objectionFont(translate("Cannot research while upgrading.")); - } - if (template.icon) - icon.sprite = modifier + "stretched:session/portraits/" + template.icon; + if (adaptedTemplate.icon) + icon.sprite = modifier + "stretched:session/portraits/" + adaptedTemplate.icon; - setPanelObjectPosition(button, position, data.rowLength); + this.occupiedPositions.add(position); + if (position >= 2 * baseData.rowLength) + this.bottomRowButtonCount++; - // Prepare to handle the top button (if any) - position -= data.rowLength; + // The panel is a bit higher than 4 * baseData.rowLength, which allows us to visibility anchor the buttons + // in the bottom row to the bottom by moving them down those few pixels. Else the gap would be at the bottom. + // This creates a small spatial separation between the "generic" techs in the bottom row and the "specific" + // techs above them. + const vOffset = position >= baseData.rowLength * 2 ? 6 : 0; + setPanelObjectPosition(button, position, baseData.rowLength, 1, 1, vOffset); + + return isActive; } - - return true; } }; @@ -1261,10 +1477,12 @@ g_SelectionPanels.Upgrade = { function initSelectionPanels() { - const unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel"); if (BarterButtonManager.IsAvailable(unitBarterPanel)) g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel); + + for (const panel in g_SelectionPanels) + g_SelectionPanels[panel].init?.(); } /** diff --git a/binaries/data/mods/public/gui/session/selection_panels_right/research_panel.xml b/binaries/data/mods/public/gui/session/selection_panels_right/research_panel.xml index c5e3a526fe..98418ea8a1 100644 --- a/binaries/data/mods/public/gui/session/selection_panels_right/research_panel.xml +++ b/binaries/data/mods/public/gui/session/selection_panels_right/research_panel.xml @@ -1,17 +1,17 @@