mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
If no player is provided then the function falls back to the viewed player, which is sometimes not the right one (like for observers), which resulted in inconsistent/incorrect data being returned. This patch fixes that.
591 lines
14 KiB
JavaScript
591 lines
14 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);
|
|
}
|
|
|
|
function getStanceDisplayName(name)
|
|
{
|
|
switch (name)
|
|
{
|
|
case "violent":
|
|
return translateWithContext("stance", "Violent");
|
|
case "aggressive":
|
|
return translateWithContext("stance", "Aggressive");
|
|
case "defensive":
|
|
return translateWithContext("stance", "Defensive");
|
|
case "passive":
|
|
return translateWithContext("stance", "Passive");
|
|
case "standground":
|
|
return translateWithContext("stance", "Standground");
|
|
default:
|
|
warn("Internationalization: Unexpected stance found: " + name);
|
|
return name;
|
|
}
|
|
}
|
|
|
|
function getStanceTooltip(name)
|
|
{
|
|
switch (name)
|
|
{
|
|
case "violent":
|
|
return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible");
|
|
case "aggressive":
|
|
return translateWithContext("stance", "Attack nearby opponents");
|
|
case "defensive":
|
|
return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location");
|
|
case "passive":
|
|
return translateWithContext("stance", "Flee if attacked");
|
|
case "standground":
|
|
return translateWithContext("stance", "Attack opponents in range, but don't move");
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
})
|
|
});
|
|
}
|