0ad/binaries/data/mods/public/gui/session/selection_panels.js

1559 lines
51 KiB
JavaScript
Raw Normal View History

/**
* Contains the layout and button settings per selection panel
*
* getItems returns a list of basic items used to fill the panel.
* This method is obligated. If the items list is empty, the panel
* won't be rendered.
*
* Then there's a loop over all items provided. In the loop,
* the item and some other standard data is added to a data object.
*
* The standard data is
* {
* "i": index
* "item": item coming from the getItems function
* "playerState": playerState
* "unitEntStates": states of the selected entities
* "rowLength": rowLength
* "numberOfItems": number of items that will be processed
* "button": gui Button object
* "icon": gui Icon object
* "guiSelection": gui button Selection overlay
* "countDisplay": gui caption space
* }
*
* Then for every data object, the setupButton function is called which
* sets the view and handlers of the button.
*/
// Cache some formation info
// Available formations per player
var g_AvailableFormations = new Map();
var g_FormationsInfo = new Map();
var g_SelectionPanels = {};
var g_SelectionPanelBarterButtonManager;
g_SelectionPanels.Alert = {
"getMaxNumberOfItems": function()
{
return 2;
},
"getItems": function(unitEntStates)
{
return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : [];
},
"setupButton": function(data)
{
data.button.onPress = function()
{
switch (data.item)
{
case "raise":
raiseAlert();
return;
case "end":
endOfAlert();
return;
default:
error("Unknown value for alert action: " + data.item);
}
};
switch (data.item)
{
case "raise":
data.icon.sprite = "stretched:session/icons/bell_level1.png";
data.button.tooltip = translate("Raise an alert!");
if (data.unitEntStates.every(state => MatchesClassList(["Civilian"], state.alertRaiser?.classes)))
data.button.tooltip += "\n" + bodyFont(translate("Alert nearby Civilians to seek refuge."));
else if (data.unitEntStates.every(state => MatchesClassList(["Trader"], state.alertRaiser?.classes)))
data.button.tooltip += "\n" + bodyFont(translate("Alert nearby Traders to seek refuge."));
else
data.button.tooltip += "\n" + bodyFont(translate("Alert nearby vulnerable units to seek refuge."));
break;
case "end":
data.icon.sprite = "stretched:session/icons/bell_level0.png";
data.button.tooltip = translate("End the alert.");
if (data.unitEntStates.every(state => MatchesClassList(["Civilian"], state.alertRaiser?.classes)))
data.button.tooltip += "\n" + bodyFont(translate("Unload nearby Civilians."));
else if (data.unitEntStates.every(state => MatchesClassList(["Trader"], state.alertRaiser?.classes)))
data.button.tooltip += "\n" + bodyFont(translate("Unload nearby Traders."));
else
data.button.tooltip += "\n" + bodyFont(translate("Unload nearby vulnerable units."));
break;
default:
error("Unknown value for alert action: " + data.item);
}
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Barter = {
"getMaxNumberOfItems": function()
{
return 5;
},
"rowLength": 5,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
// If more than `rowLength` resources, don't display icons.
Optimise GetEntityState GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
2026-02-12 06:52:21 -08:00
if (unitEntStates.every(state => !state.enablesBartering) || g_ResourceData.GetBarterableCodes().length > this.rowLength)
return [];
return g_ResourceData.GetBarterableCodes();
},
"setupButton": function(data)
{
if (g_SelectionPanelBarterButtonManager)
{
g_SelectionPanelBarterButtonManager.setViewedPlayer(data.player);
g_SelectionPanelBarterButtonManager.update();
}
return true;
}
};
g_SelectionPanels.Command = {
"getMaxNumberOfItems": function()
{
return 6;
},
"getItems": function(unitEntStates)
{
const commands = [];
for (const command in g_EntityCommands)
{
const info = getCommandInfo(command, unitEntStates);
if (!info)
continue;
info.name = command;
if (commands.push(info) >= this.getMaxNumberOfItems())
break;
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function()
{
if (data.item.callback)
data.item.callback(data.item);
else
performCommand(data.unitEntStates, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled = data.item.enabled == true;
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
const left = (data.i - data.numberOfItems / 2) * (data.button.size.bottom + 1);
Object.assign(data.button.size, {
// relative to the center ( = 50%)
"rleft": 50,
"rright": 50,
// offset from the center calculation, count on square buttons, so size.bottom is the width too
"left": left,
"right": left + data.button.size.bottom
});
return true;
}
};
g_SelectionPanels.Construction = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function()
{
return getAllBuildableEntitiesFromSelection();
},
"setupButton": function(data)
{
const template = GetTemplateData(data.item, data.player);
if (!template)
return false;
const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", {
"requirements": template.requirements,
"player": data.player
});
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, 1),
"player": data.player
});
data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); };
const showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
const tooltips = [
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
getEntityNamesFormatted,
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
].map(func => func(template));
tooltips.push(
getEntityCostTooltip(template, data.player),
getResourceDropsiteTooltip(template),
getGarrisonTooltip(template),
getTurretsTooltip(template),
getPopulationBonusTooltip(template),
getTemplateViewerOnRightClickTooltip(template)
);
const limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!requirementsMet || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
data.button.enabled = controlsPlayer(data.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Formation = {
"getMaxNumberOfItems": function()
{
return 15;
},
"rowLength": 5,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !hasClass(state, "Unit")))
return [];
if (unitEntStates.every(state => !state.unitAI || !state.unitAI.formations.length))
return [];
if (!g_AvailableFormations.has(unitEntStates[0].player))
g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player));
return g_AvailableFormations.get(unitEntStates[0].player).filter(formation => unitEntStates.some(state => !!state.unitAI && state.unitAI.formations.includes(formation)));
},
"setupButton": function(data)
{
if (!g_FormationsInfo.has(data.item))
g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
const formationOk = canMoveSelectionIntoFormation(data.item);
const unitIds = data.unitEntStates.map(state => state.id);
const formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": unitIds,
"formationTemplate": data.item
});
data.button.onPress = function()
{
performFormation(unitIds, data.item);
};
data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item);
const formationInfo = g_FormationsInfo.get(data.item);
let tooltip = translate(formationInfo.name);
if (formationInfo.tooltip)
tooltip += "\n" + bodyFont(translate(formationInfo.tooltip));
const isDefaultFormation = g_AutoFormation.isDefault(data.item);
if (data.item === NULL_FORMATION)
tooltip += "\n" + (isDefaultFormation ?
translate("Default formation is disabled.") :
translate("Right-click to disable the default formation feature."));
else
tooltip += "\n" + (isDefaultFormation ?
translate("This is the default formation, used for movement orders.") :
translate("Right-click to set this as the default formation."));
if (!formationOk && formationInfo.disabledTooltip)
tooltip += "\n" + objectionFont(translate(formationInfo.disabledTooltip));
data.button.tooltip = tooltip;
data.button.enabled = formationOk && controlsPlayer(data.player);
const grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.countDisplay.hidden = !isDefaultFormation;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Garrison = {
"getMaxNumberOfItems": function()
{
return 12;
},
"rowLength": 4,
"conflictsWith": ["Barter"],
"getItems": function(unitEntStates)
{
if (unitEntStates.every(state => !state.garrisonHolder))
return [];
const groups = new EntityGroups();
for (const state of unitEntStates)
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities);
return groups.getEntsGrouped();
},
"setupButton": function(data)
{
const entState = GetEntityState(data.item.ents[0]);
if (!entState)
return false;
data.button.onPress = function()
{
unloadTemplate(entState.selectionGroupName || entState.templateName, entState.player);
};
data.countDisplay.caption = data.item.ents.length > 1 ? data.item.ents.length : "";
const canUngarrison = controlsPlayer(data.player) || controlsPlayer(entState.player);
data.button.enabled = canUngarrison;
data.button.tooltip = (canUngarrison ?
sprintf(translate("Unload %(name)s"), { "name": getEntityNames(entState) }) + "\n" +
translate("Single-click to unload 1. Shift-click to unload all of this type.") :
getEntityNames(entState)) + "\n" +
sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[entState.player].name
});
data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(entState.player, 160);
data.button.sprite_disabled = data.button.sprite;
// Selection panel buttons only appear disabled if they
// also appear disabled to the owner of the structure.
data.icon.sprite =
(canUngarrison || g_IsObserver ? "" : "grayscale:") +
"stretched:session/portraits/" + entState.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Gate = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
const hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked);
const hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked);
if (hideLocked && hideUnlocked)
return [];
return [
{
"hidden": hideLocked,
"tooltip": translate("Lock Gate"),
"icon": "session/icons/lock_locked.png",
"locked": true
},
{
"hidden": hideUnlocked,
"tooltip": translate("Unlock Gate"),
"icon": "session/icons/lock_unlocked.png",
"locked": false
}
];
},
"setupButton": function(data)
{
data.button.onPress = function() { lockGate(data.item.locked); };
data.button.tooltip = data.item.tooltip;
data.button.enabled = controlsPlayer(data.player);
data.guiSelection.hidden = data.item.hidden;
data.icon.sprite = "stretched:" + data.item.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Pack = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
const checks = {};
for (const state of unitEntStates)
{
if (!state.pack)
continue;
if (state.pack.progress == 0)
{
if (state.pack.packed)
checks.unpackButton = true;
else
checks.packButton = true;
}
else if (state.pack.packed)
checks.unpackCancelButton = true;
else
checks.packCancelButton = true;
}
const items = [];
if (checks.packButton)
items.push({
"packing": false,
"packed": false,
"tooltip": translate("Pack"),
"callback": function() { packUnit(true); }
});
if (checks.unpackButton)
items.push({
"packing": false,
"packed": true,
"tooltip": translate("Unpack"),
"callback": function() { packUnit(false); }
});
if (checks.packCancelButton)
items.push({
"packing": true,
"packed": false,
"tooltip": translate("Cancel Packing"),
"callback": function() { cancelPackUnit(true); }
});
if (checks.unpackCancelButton)
items.push({
"packing": true,
"packed": true,
"tooltip": translate("Cancel Unpacking"),
"callback": function() { cancelPackUnit(false); }
});
return items;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
data.button.tooltip = data.item.tooltip;
if (data.item.packing)
data.icon.sprite = "stretched:session/icons/cancel.png";
else if (data.item.packed)
data.icon.sprite = "stretched:session/icons/unpack.png";
else
data.icon.sprite = "stretched:session/icons/pack.png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Queue = {
"getMaxNumberOfItems": function()
{
return 16;
},
/**
* Returns a list of all items in the productionqueue of the selection
* The first entry of every entity's production queue will come before
* the second entry of every entity's production queue
*/
"getItems": function(unitEntStates)
{
const queue = [];
let foundNew = true;
for (let i = 0; foundNew; ++i)
{
foundNew = false;
for (const state of unitEntStates)
{
if (!state.production || !state.production.queue[i])
continue;
queue.push({
"producingEnt": state.id,
"queuedItem": state.production.queue[i],
"autoqueue": state.production.autoqueue && state.production.queue[i].unitTemplate,
});
foundNew = true;
}
}
if (!queue.length)
return queue;
// Add 'ghost' items to show autoqueues.
const repeat = [];
for (const item of queue)
if (item.autoqueue)
{
const ghostItem = clone(item);
ghostItem.ghost = true;
repeat.push(ghostItem);
}
if (repeat.length)
for (let i = 0; queue.length < g_SelectionPanels.Queue.getMaxNumberOfItems(); ++i)
queue.push(repeat[i % repeat.length]);
return queue;
},
"resizePanel": function(numberOfItems, rowLength)
{
const numRows = Math.ceil(numberOfItems / rowLength);
const panel = Engine.GetGUIObjectByName("unitQueuePanel");
const buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
const margin = 4;
panel.size.top = panel.size.bottom - numRows * buttonSize - (numRows + 2) * margin;
},
"setupButton": function(data)
{
const queuedItem = data.item.queuedItem;
// Differentiate between units and techs
let template;
if (queuedItem.unitTemplate)
template = GetTemplateData(queuedItem.unitTemplate, data.player);
else if (queuedItem.technologyTemplate)
template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ);
else
{
warning("Unknown production queue template " + uneval(queuedItem));
return false;
}
data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); };
const tooltips = [getEntityNames(template)];
if (data.item.ghost)
tooltips.push(translate("The auto-queue will try to train this item later."));
if (queuedItem.neededSlots)
{
tooltips.push(objectionFont(translate("Insufficient population capacity:")));
tooltips.push(sprintf(translate("%(population)s %(neededSlots)s"), {
"population": resourceIcon("population"),
"neededSlots": queuedItem.neededSlots
}));
}
tooltips.push(getTemplateViewerOnRightClickTooltip(template));
data.button.tooltip = tooltips.join("\n");
data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";
const progressSlider = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
if (data.item.ghost)
{
data.button.enabled = false;
progressSlider.sprite = "color:0 150 250 50";
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
progressSlider.size.top = progressSlider.size.left;
}
else
{
// Show the time remaining to finish the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueTimeRemaining").caption =
Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));
progressSlider.sprite = "queueProgressSlider";
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
progressSlider.size.top = progressSlider.size.left + Math.round(queuedItem.progress * (progressSlider.size.right - progressSlider.size.left));
data.button.enabled = controlsPlayer(data.player);
Engine.GetGUIObjectByName("unitQueuePausedIcon[" + data.i + "]").hidden = !queuedItem.paused;
if (queuedItem.paused)
// Translation: String displayed when the research is paused. E.g. by being garrisoned or when not the first item in the queue.
data.button.tooltip += "\n" + translate("This item is paused.");
}
if (template.icon)
{
let modifier = "stretched:";
if (queuedItem.paused)
modifier += "color:0 0 0 127:grayscale:";
else if (data.item.ghost)
modifier += "grayscale:";
data.icon.sprite = modifier + "session/portraits/" + template.icon;
}
const showTemplateFunc = () => { showTemplateDetails(data.item.queuedItem.unitTemplate || data.item.queuedItem.technologyTemplate, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Research = {
"getMaxNumberOfItems": function()
{
return 10;
},
"rowLength": 10,
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.
2025-09-24 07:52:59 -07:00
"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)
{
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.
2025-09-24 07:52:59 -07:00
if (getNumberOfRightPanelButtons() >= this.rowLength * 2)
return [];
let ret = [];
if (unitEntStates.length == 1)
{
const entState = unitEntStates[0];
if (!entState?.researcher?.technologies)
return ret;
if (!entState.production)
warn("Researcher without ProductionQueue found: " + entState.id + ".");
return entState.researcher.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": entState.researcher.techCostMultiplier,
"researchFacilityId": entState.id,
"isUpgrading": !!entState.upgrade && entState.upgrade.isUpgrading
}));
}
const sortedEntStates = unitEntStates.sort((a, b) =>
(!b.upgrade || !b.upgrade.isUpgrading) - (!a.upgrade || !a.upgrade.isUpgrading) ||
(!a.production ? 0 : a.production.queue.length) - (!b.production ? 0 : b.production.queue.length)
);
for (const state of sortedEntStates)
{
if (!state.researcher || !state.researcher.technologies)
continue;
if (!state.production)
warn("Researcher without ProductionQueue found: " + state.id + ".");
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
const filteredTechs = state.researcher.technologies.filter(
tech => tech != null && !ret.some(
item =>
(item.tech == tech ||
item.tech.pair &&
tech.pair &&
item.tech.pair?.[0] == tech.pair?.[0] &&
item.tech.pair?.[1] == tech.pair?.[1]) &&
Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k])
));
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.
2025-09-24 07:52:59 -07:00
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems())
ret = ret.concat(filteredTechs.map(tech => ({
"tech": tech,
"techCostMultiplier": state.researcher.techCostMultiplier,
"researchFacilityId": state.id,
"isUpgrading": !!state.upgrade && state.upgrade.isUpgrading
})));
}
return ret;
},
"hideItem": function(i, rowLength) // Called when no item is found
{
Engine.GetGUIObjectByName("unitResearchButton[" + i + "]").hidden = true;
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.
2025-09-24 07:52:59 -07:00
// 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)
{
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.
2025-09-24 07:52:59 -07:00
this.hideItem(data.i, data.rowLength);
return false;
}
// 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();
const playerState = GetSimState().players[data.player];
if (data.item.tech.pair)
{
const firstTemplate = GetTechnologyData(data.item.tech.pair[0], playerState.civ);
const secondTemplate = GetTechnologyData(data.item.tech.pair[1], playerState.civ);
// 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;
}
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.
2025-09-24 07:52:59 -07:00
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}")
{
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);
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.
2025-09-24 07:52:59 -07:00
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;
}
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.
2025-09-24 07:52:59 -07:00
Engine.GetGUIObjectByName("unitResearchVerticalPairIcon[" + data.i + "]").hidden = true;
Engine.GetGUIObjectByName("unitResearchHorizontalPairIcon[" + data.i + "]").hidden = true;
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.
2025-09-24 07:52:59 -07:00
// 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)
{
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.
2025-09-24 07:52:59 -07:00
// 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;
}
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.
2025-09-24 07:52:59 -07:00
if (placeInBottomRow)
{
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.
2025-09-24 07:52:59 -07:00
// 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;
}
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.
2025-09-24 07:52:59 -07:00
// 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", {
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.
2025-09-24 07:52:59 -07:00
"cost": adaptedTemplate.cost,
"player": baseData.player
});
const requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
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.
2025-09-24 07:52:59 -07:00
"tech": techName,
"player": baseData.player
});
const tooltips = [
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
getEntityNamesFormatted,
getEntityTooltip,
getEntityCostTooltip,
getTemplateViewerOnRightClickTooltip
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.
2025-09-24 07:52:59 -07:00
].map(func => func(adaptedTemplate));
if (!requirementsPassed)
{
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.
2025-09-24 07:52:59 -07:00
let tip = adaptedTemplate.requirementsTooltip;
const reqs = adaptedTemplate.reqs;
for (const req of reqs)
{
if (!req.entities)
continue;
const entityCounts = [];
for (const entity of req.entities)
{
let current = 0;
switch (entity.check)
{
case "count":
current = playerState.classCounts[entity.class] || 0;
break;
case "variants":
current = playerState.typeCountsByClass[entity.class] ?
Object.keys(playerState.typeCountsByClass[entity.class]).length : 0;
break;
default:
error("Unknow value in entity requirement check: " + entity.check);
}
const remaining = entity.number - current;
if (remaining < 1)
continue;
entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), {
"number": remaining,
"class": translate(entity.class)
}));
}
tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), {
"entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", "))
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
});
}
tooltips.push(objectionFont(tip));
}
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
tooltips.push(getNeededResourcesTooltip(neededResources));
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (t => function()
{
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.
2025-09-24 07:52:59 -07:00
addResearchToQueue(baseData.item.researchFacilityId, t);
})(techName);
const showTemplateFunc = (t => function()
{
showTemplateDetails(
t,
baseData.unitEntStates.find(state => state.id == baseData.item.researchFacilityId).nativeCiv
);
});
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.
2025-09-24 07:52:59 -07:00
button.onPressRight = showTemplateFunc(techName);
button.onPressRightDisabled = showTemplateFunc(techName);
button.hidden = false;
let modifier = "";
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.
2025-09-24 07:52:59 -07:00
let isActive = true;
if (!requirementsPassed)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
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.
2025-09-24 07:52:59 -07:00
isActive = false;
}
else if (neededResources)
{
button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
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.
2025-09-24 07:52:59 -07:00
button.enabled = controlsPlayer(baseData.player);
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.
2025-09-24 07:52:59 -07:00
if (baseData.item.isUpgrading)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
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.
2025-09-24 07:52:59 -07:00
isActive = false;
button.tooltip += "\n" + objectionFont(translate("Cannot research while upgrading."));
}
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.
2025-09-24 07:52:59 -07:00
if (adaptedTemplate.icon)
icon.sprite = modifier + "stretched:session/portraits/" + adaptedTemplate.icon;
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.
2025-09-24 07:52:59 -07:00
this.occupiedPositions.add(position);
if (position >= 2 * baseData.rowLength)
this.bottomRowButtonCount++;
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.
2025-09-24 07:52:59 -07:00
// 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);
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.
2025-09-24 07:52:59 -07:00
return isActive;
}
}
};
g_SelectionPanels.Selection = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"getItems": function(unitEntStates)
{
if (unitEntStates.length < 2)
return [];
return g_Selection.groups.getEntsGrouped();
},
"setupButton": function(data)
{
const entState = GetEntityState(data.item.ents[0]);
if (!entState)
return false;
for (const ent of data.item.ents)
{
const state = GetEntityState(ent);
if (state.resourceCarrying && state.resourceCarrying.length !== 0)
{
if (!data.carried)
data.carried = {};
const 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 = {};
const amount = state.trader.goods.amount;
const type = state.trader.goods.type;
let totalGain = amount.traderGain;
if (amount.market1Gain)
totalGain += amount.market1Gain;
if (amount.market2Gain)
totalGain += amount.market2Gain;
if (data.carried[type])
data.carried[type] += totalGain;
else
data.carried[type] = totalGain;
}
}
const unitOwner = GetEntityState(data.item.ents[0]).player;
let tooltip = getEntityNames(entState);
if (data.carried)
tooltip += "\n" + Object.keys(data.carried).map(res =>
resourceIcon(res) + data.carried[res]
).join(" ");
if (g_IsObserver)
tooltip += "\n" + sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[unitOwner].name
});
data.button.tooltip = tooltip;
data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(unitOwner, 160);
data.guiSelection.hidden = !g_IsObserver;
data.countDisplay.caption = data.item.ents.length > 1 ? data.item.ents.length : "";
data.button.onPress = function()
{
if (Engine.HotkeyIsPressed("session.deselectgroup"))
removeFromSelectionGroup(data.item.key);
else
makePrimarySelectionGroup(data.item.key);
};
data.button.onPressRight = function() { removeFromSelectionGroup(data.item.key); };
if (entState.icon)
data.icon.sprite = "stretched:session/portraits/" + entState.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Stance = {
"getMaxNumberOfItems": function()
{
return 5;
},
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")) ||
unitEntStates.every(state => state.turretable && state.turretable.holder !== INVALID_ENTITY))
return [];
return Object.keys(this.stancesData);
},
"setupButton": function(data)
{
const unitIds = data.unitEntStates.map(state => state.id);
data.button.onPress = function() { performStance(unitIds, data.item); };
data.button.tooltip = this.stancesData[data.item].Name + "\n" + bodyFont(this.stancesData[data.item].Tooltip);
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": unitIds,
"stance": data.item
});
data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
},
"stancesData": {
"violent": {
"Name": translateWithContext("stance", "Violent"),
"Tooltip": translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible")
},
"aggressive": {
"Name": translateWithContext("stance", "Aggressive"),
"Tooltip": translateWithContext("stance", "Attack nearby opponents")
},
"defensive": {
"Name": translateWithContext("stance", "Defensive"),
"Tooltip": translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location")
},
"passive": {
"Name": translateWithContext("stance", "Passive"),
"Tooltip": translateWithContext("stance", "Flee if attacked")
},
"standground": {
"Name": translateWithContext("stance", "Standground"),
"Tooltip": translateWithContext("stance", "Attack opponents in range, but don't move")
}
}
};
g_SelectionPanels.Training = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function()
{
return getAllTrainableEntitiesFromSelection();
},
"setupButton": function(data)
{
const template = GetTemplateData(data.item, data.player);
if (!template)
return false;
const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", {
"requirements": template.requirements,
"player": data.player
});
const unitIds = data.unitEntStates.map(status => status.id);
const [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingStatus(unitIds, data.item, data.playerState);
const trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, trainNum),
"player": data.player
});
data.button.onPress = function()
{
addTrainingToQueue(unitIds, data.item, data.playerState);
};
const showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
data.countDisplay.caption = trainNum > 1 ? trainNum : "";
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
let tooltips = [
"[font=\"sans-bold-16\"]" +
colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
"[/font]" + " " + getEntityNamesFormatted(template),
getVisibleEntityClassesFormatted(template),
getAurasTooltip(template),
getEntityTooltip(template),
getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
];
const limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type));
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
tooltips = tooltips.concat([
getHealthTooltip,
getAttackTooltip,
getHealerTooltip,
getResistanceTooltip,
getGarrisonTooltip,
getTurretsTooltip,
getProjectilesTooltip,
getSpeedTooltip,
getResourceDropsiteTooltip
].map(func => func(template)));
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
tooltips.push(getTemplateViewerOnRightClickTooltip());
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
tooltips.push(
formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch),
getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
Tooltip overhaul. Also contains a patch by fatherbushido for attackRateDetails and getAttackTooltip to not show something broken for buildingAI units that can capture, fixes #4061 (see #4000). Make tooltip functions uniform. Pass template everywhere instead of template.armour in getArmorTooltip rate in getRepairRateTooltip and getBuildRateTooltip and entState in attackRateDetails. Add an early return for every tooltip function. Use empty string instead of "Armor: (None)" for trees etc.. Don't prefix tooltip return values with "\n", but let the user of that function add them. Thus make tooltip concatenation much nicer (f.e. draw.js). Use a loop instead of duplicating per damage type in damageTypesToText. Add font functions to avoid duplicating tag code. Merge sprintf's and inline variables. Add few TODOs. Fix some strings: Use "%(specificName)s %(fontStart)s(%(genericName)s)%(fontEnd)s") instead of "(" + foo + ")" ... Use existing "%(percentage)s%%" instead of foo + "%" in armorLevelToPercentageString. Remove duplication by calling/introducing shared functions (getEntityTooltip, getHealthTooltip, getGatherTooltip, getVisibleEntityClassesFormatted), unused function damageTypeDetails which was also a duplicate of damageTypesToText, unused function damageValues, some warns that are equivalent to errors they attempt to cover up (getAttackTypeLabel, getCostComponentDisplayIcon, getEntityNames, getEntityNamesFormatted), some unused variables, "???" and translate("???"). Don't fix translate("Foo:") strings to avoid a lot of translation work. This was SVN commit r18454.
2016-06-29 18:16:09 -07:00
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!requirementsMet || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else
{
data.button.enabled = controlsPlayer(data.player);
if (neededResources)
modifier = resourcesToAlphaMask(neededResources) + ":";
}
if (data.unitEntStates.every(state => state.upgrade && state.upgrade.isUpgrading))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
data.button.tooltip += "\n" + objectionFont(translate("Cannot train while upgrading."));
}
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
const index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Upgrade = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
Optimise GetEntityState GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
2026-02-12 06:52:21 -08:00
if (unitEntStates.some(state => state.templateName != unitEntStates[0].templateName))
return false;
Optimise GetEntityState GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
2026-02-12 06:52:21 -08:00
return unitEntStates[0].upgrade?.options;
},
"setupButton": function(data)
{
const template = GetTemplateData(data.item.entity, data.player);
if (!template)
return false;
const progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]");
progressOverlay.hidden = true;
const requirementsMet = !data.item.requirements ||
Engine.GuiInterfaceCall("AreRequirementsMet", {
"requirements": data.item.requirements,
"player": data.player
});
const limits = getEntityLimitAndCount(data.playerState, data.item.entity);
Optimise GetEntityState GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
2026-02-12 06:52:21 -08:00
const upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.templateName == data.item.entity);
const upgradableEntStates = data.unitEntStates.filter(state =>
!state.upgrade.progress &&
(!state.production || !state.production.queue || !state.production.queue.length));
const neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(data.item, upgradableEntStates.length),
"player": data.player
});
let tooltip;
let modifier = "";
if (!upgradingEntStates.length && upgradableEntStates.length)
{
const primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
let secondaryName;
if (g_ShowSecondaryNames)
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
const tooltips = [];
if (g_ShowSecondaryNames)
{
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s). %(tooltip)s"), {
"primaryName": primaryName,
"secondaryName": secondaryName,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s)."), {
"primaryName": primaryName,
"secondaryName": secondaryName
}));
}
else
{
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s. %(tooltip)s"), {
"primaryName": primaryName,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s."), {
"primaryName": primaryName
}));
}
tooltips.push(
getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length),
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
getRequirementsTooltip(requirementsMet, data.item.requirements, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources),
getTemplateViewerOnRightClickTooltip()
);
tooltip = tooltips.filter(tip => tip).join("\n");
data.button.onPress = function()
{
upgradeEntity(
data.item.entity,
upgradableEntStates.map(state => state.id));
};
if (!requirementsMet || limits.canBeAddedCount == 0 &&
Optimise GetEntityState GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
2026-02-12 06:52:21 -08:00
!upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.templateName, state.player)))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier = resourcesToAlphaMask(neededResources) + ":";
}
data.countDisplay.caption = upgradableEntStates.length > 1 ? upgradableEntStates.length : "";
}
else if (upgradingEntStates.length)
{
tooltip = translate("Cancel Upgrading");
data.button.onPress = function() { cancelUpgradeEntity(); };
data.countDisplay.caption = upgradingEntStates.length > 1 ? upgradingEntStates.length : "";
let progress = 0;
for (const state of upgradingEntStates)
progress = Math.max(progress, state.upgrade.progress || 1);
// TODO This is bad: we assume the progressOverlay is square
progressOverlay.size.top = progressOverlay.size.bottom + Math.round((1 - progress) * (progressOverlay.size.left - progressOverlay.size.right));
progressOverlay.hidden = false;
}
else
{
tooltip = objectionFont(translatePlural(
"Cannot upgrade when the entity is training, researching or already upgrading.",
"Cannot upgrade when all entities are training, researching or already upgrading.",
data.unitEntStates.length));
data.button.onPress = function() {};
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
data.button.enabled = controlsPlayer(data.player);
data.button.tooltip = tooltip;
const showTemplateFunc = () => { showTemplateDetails(data.item.entity, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
data.icon.sprite = modifier + "stretched:session/" +
(data.item.icon || "portraits/" + template.icon);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
function initSelectionPanels()
{
const unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel");
if (BarterButtonManager.IsAvailable(unitBarterPanel))
g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel);
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.
2025-09-24 07:52:59 -07:00
for (const panel in g_SelectionPanels)
g_SelectionPanels[panel].init?.();
}
/**
* Pauses game and opens the template details viewer for a selected entity or technology.
*
* Technologies don't have a set civ, so we pass along the native civ of
* the template of the entity that's researching it.
*
* @param {string} [civCode] - The template name of the entity that researches the selected technology.
*/
async function showTemplateDetails(templateName, civCode)
{
if (inputState != INPUT_NORMAL)
return;
g_PauseControl.implicitPause();
await Engine.OpenChildPage(
"page_viewer.xml",
{
"templateName": templateName,
"civ": civCode
});
resumeGame();
}
/**
* If two panels need the same space, so they collide,
* the one appearing first in the order is rendered.
*
* Note that the panel needs to appear in the list to get rendered.
*/
const g_PanelsOrder = [
// LEFT PANE
"Barter", // Must always be visible on markets
"Garrison", // More important than Formation, as you want to see the garrisoned units in ships
"Alert",
"Formation",
"Stance", // Normal together with formation
// RIGHT PANE
"Gate", // Must always be shown on gates
"Pack", // Must always be shown on packable entities
"Upgrade", // Must always be shown on upgradable entities
"Training",
"Construction",
"Research", // Normal together with training
// UNIQUE PANES (importance doesn't matter)
"Command",
"Queue",
"Selection",
];