0ad/binaries/data/mods/public/gui/session/unit_commands.js
Vantha fb38c8781b
Some checks failed
checkrefs / lfscheck (push) Has been cancelled
checkrefs / checkrefs (push) Has been cancelled
lint / cppcheck (push) Has been cancelled
lint / copyright (push) Has been cancelled
lint / jenkinsfiles (push) Has been cancelled
pre-commit / build (push) Has been cancelled
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.
2026-07-06 05:01:12 +02:00

242 lines
7.4 KiB
JavaScript

// The number of currently visible buttons (used to optimise showing/hiding)
var g_unitPanelButtons = {
"Selection": 0,
"Queue": 0,
"Formation": 0,
"Garrison": 0,
"Training": 0,
"Research": 0,
"Alert": 0,
"Barter": 0,
"Construction": 0,
"Command": 0,
"Stance": 0,
"Gate": 0,
"Pack": 0,
"Upgrade": 0
};
/**
* Set the position of a panel object according to the index,
* from left to right, from top to bottom.
* Will wrap around to subsequent rows if the index
* is larger than rowLength.
*/
function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin = 1, vOffset = 0, hOffset = 0)
{
const oWidth = object.size.right - object.size.left;
const oHeight = object.size.bottom - object.size.top;
const left = (index % rowLength) * (oWidth + hMargin) + hOffset;
const top = (Math.floor(index / rowLength)) * (oHeight + vMargin) + vOffset;
Object.assign(object.size, {
// horizontal position
"left": left,
"right": left + oWidth,
// vertical position
"top": top,
"bottom": top + oHeight
});
}
/**
* Helper function for updateUnitCommands; sets up "unit panels"
* (i.e. panels with rows of icons) for the currently selected unit.
*
* @param guiName Short identifier string of this panel. See g_SelectionPanels.
* @param unitEntStates Entity states of the selected units
* @param playerState Player state
*/
function setupUnitPanel(guiName, unitEntStates, playerState)
{
if (!g_SelectionPanels[guiName])
{
error("unknown guiName used '" + guiName + "'");
return;
}
const items = g_SelectionPanels[guiName].getItems(unitEntStates) || [];
const numberOfItems = Math.min(items.length, g_SelectionPanels[guiName].getMaxNumberOfItems());
const rowLength = g_SelectionPanels[guiName].rowLength || 8;
if (numberOfItems && g_SelectionPanels[guiName].resizePanel)
g_SelectionPanels[guiName].resizePanel(numberOfItems, rowLength);
for (let i = 0; i < numberOfItems; ++i)
{
const data = {
"i": i,
"item": items[i],
"playerState": playerState,
"player": unitEntStates[0].player,
"unitEntStates": unitEntStates,
"rowLength": rowLength,
"numberOfItems": numberOfItems,
// depending on the XML, some of the GUI objects may be undefined
"button": Engine.TryGetGUIObjectByName("unit" + guiName + "Button[" + i + "]"),
"icon": Engine.TryGetGUIObjectByName("unit" + guiName + "Icon[" + i + "]"),
"guiSelection": Engine.TryGetGUIObjectByName("unit" + guiName + "Selection[" + i + "]"),
"countDisplay": Engine.TryGetGUIObjectByName("unit" + guiName + "Count[" + i + "]")
};
if (data.button)
{
data.button.hidden = false;
data.button.enabled = true;
data.button.tooltip = "";
data.button.caption = "";
}
if (g_SelectionPanels[guiName].setupButton &&
!g_SelectionPanels[guiName].setupButton(data))
continue;
// TODO: we should require all entities to have icons, so this case never occurs
if (data.icon && !data.icon.sprite)
data.icon.sprite = "BackgroundBlack";
}
// Hide any buttons we're no longer using
for (let i = numberOfItems; i < g_unitPanelButtons[guiName]; ++i)
if (g_SelectionPanels[guiName].hideItem)
g_SelectionPanels[guiName].hideItem(i, rowLength);
else
{
const button = Engine.TryGetGUIObjectByName("unit" + guiName + "Button[" + i + "]");
if (button) button.hidden = true;
}
g_unitPanelButtons[guiName] = numberOfItems;
g_SelectionPanels[guiName].used = numberOfItems > 0;
}
/**
* Updates the selection panels where buttons are supposed to
* depend on the context.
* Runs in the main session loop via updateSelectionDetails().
* Delegates to setupUnitPanel to set up individual subpanels,
* appropriately activated depending on the selected unit's state.
*
* @param entStates Entity states of the selected units
* @param supplementalDetailsPanel Reference to the
* "supplementalSelectionDetails" GUI Object
* @param commandsPanel Reference to the "commandsPanel" GUI Object
*/
function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel)
{
for (const panel in g_SelectionPanels)
{
g_SelectionPanels[panel].used = false;
g_SelectionPanels[panel].reset?.();
}
// Get player state to check some constraints
// e.g. presence of a hero or build limits.
const playerStates = GetSimState().players;
const playerState = playerStates[Engine.GetPlayerID()];
if (g_IsObserver || entStates.every(entState =>
controlsPlayer(entState.player) &&
(!entState.identity || entState.identity.controllable)) ||
playerState.controlsAll)
{
for (const guiName of g_PanelsOrder)
{
if (!g_SelectionPanels[guiName].conflictsWith ||
g_SelectionPanels[guiName].conflictsWith.every(p => !g_SelectionPanels[p].used))
setupUnitPanel(guiName, entStates, playerStates[entStates[0].player]);
}
supplementalDetailsPanel.hidden = false;
commandsPanel.hidden = false;
}
else
{
// Always show what entities are selected, no matter if they can be controlled.
setupUnitPanel("Selection", entStates, playerStates[entStates[0].player]);
// Always show the commands since they might not require the entities to be owned.
// TODO: This panel here is NOT related to the commandsPanel GUI object. The naming should be improved.
setupUnitPanel("Command", entStates, playerState);
if (playerState.isMutualAlly[entStates[0].player])
{
// TODO if there's a second panel needed for a different player
// we should consider adding the players list to g_SelectionPanels
setupUnitPanel("Garrison", entStates, playerState);
supplementalDetailsPanel.hidden = !g_SelectionPanels.Garrison.used;
commandsPanel.hidden = true;
}
else
{
supplementalDetailsPanel.hidden = true;
commandsPanel.hidden = true;
}
}
// Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time)
for (const panelName in g_SelectionPanels)
Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = !g_SelectionPanels[panelName].used;
}
// Force hide commands panels
function hideUnitCommands()
{
for (var panelName in g_SelectionPanels)
Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = true;
}
// Get all of the available entities which can be trained by the selected entities
function getAllTrainableEntities(selection)
{
let trainableEnts = [];
// Get all buildable and trainable entities
for (const ent of selection)
{
const state = GetEntityState(ent);
if (state?.trainer?.entities?.length)
{
if (!state.production)
warn("Trainer without Production Queue found: " + ent + ".");
trainableEnts = trainableEnts.concat(state.trainer.entities);
}
}
// Remove duplicates
removeDupes(trainableEnts);
return trainableEnts;
}
function getAllTrainableEntitiesFromSelection()
{
if (!g_allTrainableEntities)
g_allTrainableEntities = getAllTrainableEntities(g_Selection.toList());
return g_allTrainableEntities;
}
// Get all of the available entities which can be built by the selected entities
function getAllBuildableEntities(selection)
{
return Engine.GuiInterfaceCall("GetAllBuildableEntities", { "entities": selection });
}
function getAllBuildableEntitiesFromSelection()
{
if (!g_allBuildableEntities)
g_allBuildableEntities = getAllBuildableEntities(g_Selection.toList());
return g_allBuildableEntities;
}
function getNumberOfRightPanelButtons()
{
var sum = 0;
for (const prop of ["Construction", "Training", "Pack", "Gate", "Upgrade"])
if (g_SelectionPanels[prop].used)
sum += g_unitPanelButtons[prop];
return sum;
}