0ad/binaries/data/mods/public/gui/session/selection_panels_helpers.js
Vantha a954b7742e
Remove selectableStance from entity states
The array was wastefully created and passed to the GUI every single
turn for every single selected unit.
Since the GUI defines the (selectable) stances' names and tooltips, it
already knew them anyway, so they don't even need to be passed from the
simulation to it in the first place.
2026-08-12 11:55:14 +02:00

552 lines
13 KiB
JavaScript

/**
* @file Contains all helper functions that are needed only for selection_panels.js
* and some that are needed for hotkeys, but not for anything inside input.js.
*/
const UPGRADING_NOT_STARTED = -2;
const UPGRADING_CHOSEN_OTHER = -1;
function canMoveSelectionIntoFormation(formationTemplate)
{
if (formationTemplate == NULL_FORMATION)
return true;
if (!(formationTemplate in g_canMoveIntoFormation))
g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationTemplate": formationTemplate
});
return g_canMoveIntoFormation[formationTemplate];
}
function hasSameRestrictionCategory(templateName1, templateName2, player)
{
const template1 = GetTemplateData(templateName1, player);
const template2 = GetTemplateData(templateName2, player);
if (template1.trainingRestrictions && template2.trainingRestrictions)
return template1.trainingRestrictions.category == template2.trainingRestrictions.category;
if (template1.buildRestrictions && template2.buildRestrictions)
return template1.buildRestrictions.category == template2.buildRestrictions.category;
return false;
}
/**
* Returns a "color:255 0 0 Alpha" string based on how many resources are needed.
*/
function resourcesToAlphaMask(neededResources)
{
let totalCost = 0;
for (const resource in neededResources)
totalCost += +neededResources[resource];
return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50);
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
if (trainEntLimit == undefined)
return "";
var text = sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), {
"count": trainEntCount,
"limit": trainEntLimit
});
if (trainEntCount >= trainEntLimit)
text = objectionFont(text);
for (var c in trainEntLimitChangers)
{
if (!trainEntLimitChangers[c])
continue;
const string = trainEntLimitChangers[c] > 0 ?
translate("%(changer)s enlarges the limit with %(change)s.") :
translate("%(changer)s lessens the limit with %(change)s.");
text += "\n" + sprintf(string, {
"changer": translate(c),
"change": trainEntLimitChangers[c]
});
}
return text;
}
/**
* Format template match count/limit message for the tooltip.
*
* @param {number} matchEntLimit - The limit of the entity.
* @param {number} matchEntCount - The count of the entity.
* @param {string} type - The type of the action (i.e. "build" or "training").
*
* @return {string} - The string to show the user with information regarding the limit of this template.
*/
function formatMatchLimitString(matchEntLimit, matchEntCount, type)
{
if (matchEntLimit == undefined)
return "";
const passedLimit = matchEntCount >= matchEntLimit;
const count = matchEntLimit - matchEntCount;
let text;
if (type == "build")
{
if (matchEntLimit == 1)
text = translate("Can be constructed only once.");
else if (passedLimit)
text = sprintf(translatePlural(
"Could only be constructed %(limit)s time.",
"Could only be constructed %(limit)s times.",
matchEntLimit),
{ "limit": matchEntLimit });
else
text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), {
"count": count
});
}
else if (type == "training")
{
if (passedLimit)
text = sprintf(translatePlural("Could only be trained once.", "Could only be trained %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be trained only once.");
else
text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), {
"count": count
});
}
else
{
if (passedLimit)
text = sprintf(translatePlural("Could only be created once.", "Could only be created %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be created only once.");
else
text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), {
"count": count
});
}
return passedLimit ? objectionFont(text) : text;
}
/**
* Format batch training string for the tooltip
* Examples:
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 5"
* buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 10 (2*5)"
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
* "Shift-click to train 27 (15 + 12)"
*/
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
// Don't show the batch training tooltip if either units of this type can't be trained at all
// or only one unit can be trained
if (totalBatchTrainingCount < 2)
return "";
let fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), {
"buildings": buildingsCountToTrainFullBatch,
"batchSize": fullBatchSize
});
else if (buildingsCountToTrainFullBatch == 1)
fullBatchesString = fullBatchSize;
// We need to display the batch details part if there is either more than
// one structure with full batch or one structure with the full batch and
// another with a partial batch
let batchString;
if (buildingsCountToTrainFullBatch > 1 ||
buildingsCountToTrainFullBatch == 1 && remainderBatch > 0)
if (remainderBatch > 0)
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s).");
else
batchString = translate("%(action)s to train %(number)s (%(fullBatch)s).");
else
batchString = translate("%(action)s to train %(number)s.");
return "[font=\"sans-13\"]" +
setStringTags(
sprintf(batchString, {
"action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]",
"number": totalBatchTrainingCount,
"fullBatch": fullBatchesString,
"remainderBatch": remainderBatch
}),
g_HotkeyTags) +
"[/font]";
}
/**
* Camera jumping: when the user presses a hotkey the current camera location is marked.
* When pressing another camera jump hotkey the camera jumps back to that position.
* When the camera is already roughly at that location, jump back to where it was previously.
*/
var g_JumpCameraPositions = [];
var g_JumpCameraLast;
function jumpCamera(index)
{
const position = g_JumpCameraPositions[index];
if (!position)
return;
const threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
const cameraPivot = Engine.GetCameraPivot();
if (g_JumpCameraLast &&
Math.abs(cameraPivot.x - position.x) < threshold &&
Math.abs(cameraPivot.z - position.z) < threshold)
{
Engine.CameraMoveTo(g_JumpCameraLast.x, g_JumpCameraLast.z);
}
else
{
g_JumpCameraLast = cameraPivot;
Engine.CameraMoveTo(position.x, position.z);
}
}
function setJumpCamera(index)
{
g_JumpCameraPositions[index] = Engine.GetCameraPivot();
}
/**
* Called by GUI when user clicks a research button.
*/
function addResearchToQueue(entity, researchType)
{
Engine.PostNetworkCommand({
"type": "research",
"entity": entity,
"template": researchType,
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
/**
* Called by GUI when user clicks a production queue item.
*/
function removeFromProductionQueue(entity, id)
{
Engine.PostNetworkCommand({
"type": "stop-production",
"entity": entity,
"id": id
});
}
/**
* Called by unit selection buttons.
*/
function makePrimarySelectionGroup(templateName)
{
g_Selection.makePrimarySelection(templateName);
}
function removeFromSelectionGroup(templateName)
{
g_Selection.removeGroupFromSelection(templateName);
}
function performCommand(entStates, commandName)
{
if (!entStates.length)
return;
if (getCommandInfo(commandName, entStates))
g_EntityCommands[commandName].execute(entStates);
}
function performFormation(entities, formationTemplate)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "formation",
"entities": entities,
"formation": formationTemplate
});
}
function performStance(entities, stanceName)
{
if (!entities)
return;
Engine.PostNetworkCommand({
"type": "stance",
"entities": entities,
"name": stanceName
});
}
function lockGate(lock)
{
Engine.PostNetworkCommand({
"type": "lock-gate",
"entities": g_Selection.toList(),
"lock": lock
});
}
function packUnit(pack)
{
Engine.PostNetworkCommand({
"type": "pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function cancelPackUnit(pack)
{
Engine.PostNetworkCommand({
"type": "cancel-pack",
"entities": g_Selection.toList(),
"pack": pack,
"queued": false
});
}
function upgradeEntity(Template, selection)
{
Engine.PostNetworkCommand({
"type": "upgrade",
"entities": selection,
"template": Template,
"queued": false
});
}
function cancelUpgradeEntity()
{
Engine.PostNetworkCommand({
"type": "cancel-upgrade",
"entities": g_Selection.toList(),
"queued": false
});
}
/**
* Focus the camera on the entity and follow if it's a unit.
* If that's not possible, stop any current follow.
*/
function setCameraFollow(entity)
{
const entState = entity && GetEntityState(entity);
if (entState && hasClass(entState, "Unit"))
Engine.CameraFollow(entity);
else if (entState?.position)
Engine.CameraMoveTo(entState.position.x, entState.position.z);
else
Engine.CameraFollow(0);
}
function stopUnits(entities)
{
Engine.PostNetworkCommand({
"type": "stop",
"entities": entities,
"queued": false
});
}
function unloadTemplate(template, owner)
{
Engine.PostNetworkCommand({
"type": "unload-template",
"all": Engine.HotkeyIsPressed("session.unloadtype"),
"template": template,
"owner": owner,
// Filter out all entities that aren't garrisonable.
"garrisonHolders": g_Selection.filter(ent =>
{
const state = GetEntityState(ent);
return state && !!state.garrisonHolder;
})
});
}
function unloadAll()
{
const garrisonHolders = g_Selection.filter(e =>
{
const state = GetEntityState(e);
return state && !!state.garrisonHolder;
});
if (!garrisonHolders.length)
return;
const ownEnts = [];
const otherEnts = [];
for (const ent of garrisonHolders)
{
if (controlsPlayer(GetEntityState(ent).player))
ownEnts.push(ent);
else
otherEnts.push(ent);
}
if (ownEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all",
"garrisonHolders": ownEnts
});
if (otherEnts.length)
Engine.PostNetworkCommand({
"type": "unload-all-by-owner",
"garrisonHolders": otherEnts
});
}
function unloadAllTurrets()
{
const turretHolders = g_Selection.filter(e =>
{
const state = GetEntityState(e);
return state && !!state.turretHolder;
});
if (!turretHolders.length)
return;
const ownedHolders = [];
const ejectables = [];
for (const ent of turretHolders)
{
const turretHolderState = GetEntityState(ent);
if (controlsPlayer(turretHolderState.player))
ownedHolders.push(ent);
else
{
for (const turret of turretHolderState.turretHolder.turretPoints.map(tp => tp.entity))
if (turret && controlsPlayer(GetEntityState(turret).player))
ejectables.push(turret);
}
}
if (ejectables.length)
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": ejectables
});
if (ownedHolders.length)
Engine.PostNetworkCommand({
"type": "unload-turrets",
"entities": ownedHolders
});
}
function leaveTurretPoints()
{
const entities = g_Selection.filter(entity =>
{
const entState = GetEntityState(entity);
return entState && entState.turretable &&
entState.turretable.holder != INVALID_ENTITY;
});
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": entities
});
}
function backToWork()
{
Engine.PostNetworkCommand({
"type": "back-to-work",
// Filter out all entities that can't go back to work.
"entities": g_Selection.filter(ent =>
{
const state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.hasWorkOrders;
})
});
}
function removeGuard()
{
Engine.PostNetworkCommand({
"type": "remove-guard",
// Filter out all entities that are currently guarding/escorting.
"entities": g_Selection.filter(ent =>
{
const state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.isGuarding;
})
});
}
function raiseAlert()
{
Engine.PostNetworkCommand({
"type": "alert-raise",
"entities": g_Selection.filter(ent =>
{
const state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
function endOfAlert()
{
Engine.PostNetworkCommand({
"type": "alert-end",
"entities": g_Selection.filter(ent =>
{
const state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
function turnAutoQueueOn()
{
Engine.PostNetworkCommand({
"type": "autoqueue-on",
"entities": g_Selection.filter(ent =>
{
const state = GetEntityState(ent);
return !!state?.trainer?.entities?.length &&
!state.production.autoqueue;
})
});
}
function turnAutoQueueOff()
{
Engine.PostNetworkCommand({
"type": "autoqueue-off",
"entities": g_Selection.filter(ent =>
{
const state = GetEntityState(ent);
return !!state?.trainer?.entities?.length &&
state.production.autoqueue;
})
});
}