mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Instead of storing a single flat list of positions and data per building, RallyPoint now stores them keyed by player ID. This lets mutual allies independently set and display rally points on each other's structures. The GUI now allows selecting allied buildings with a rally point and only shows the viewing player's own rally point data. GuiInterface gets an OnUpdate handler to keep displayed positions in sync when rally point targets move. GetRallyPointCommands now takes raw position and data arrays instead of a component reference. The network command field is also renamed from "entities" to "structures". Fixes #3115
2093 lines
58 KiB
JavaScript
2093 lines
58 KiB
JavaScript
/**
|
|
* Specifies which template should indicate the target location of a player command,
|
|
* given a command type.
|
|
*/
|
|
var g_TargetMarker = {
|
|
"move": "special/target_marker",
|
|
"map_flare": "special/flare_target_marker"
|
|
};
|
|
|
|
/**
|
|
* Sound we play when displaying a flare.
|
|
*/
|
|
var g_FlareSound = "audio/interface/alarm/alarmally_1.ogg";
|
|
|
|
/**
|
|
* Sound we play when setting a waypoint or rally point.
|
|
*/
|
|
var g_RallySound = "audio/interface/ui/rally_click_01.ogg";
|
|
|
|
/**
|
|
* Which enemy entity types will be attacked on sight when patroling.
|
|
*/
|
|
var g_PatrolTargets = ["Unit"];
|
|
|
|
const g_DisabledTags = { "color": "255 140 0" };
|
|
|
|
/**
|
|
* List of different actions units can execute,
|
|
* this is mostly used to determine which actions can be executed
|
|
*
|
|
* "execute" is meant to send the command to the engine
|
|
*
|
|
* The next functions will always return false
|
|
* in case you have to continue to seek
|
|
* (i.e. look at the next entity for getActionInfo, the next
|
|
* possible action for the actionCheck ...)
|
|
* They will return an object when the searching is finished
|
|
*
|
|
* "getActionInfo" is used to determine if the action is possible,
|
|
* and also give visual feedback to the user (tooltips, cursors, ...)
|
|
*
|
|
* "preSelectedActionCheck" is used to select actions when the gui buttons
|
|
* were used to set them, but still require a target (like the guard button)
|
|
*
|
|
* "hotkeyActionCheck" is used to check the possibility of actions when
|
|
* a hotkey is pressed
|
|
*
|
|
* "actionCheck" is used to check the possibilty of actions without specific
|
|
* command. For that, the specificness variable is used
|
|
*
|
|
* "specificness" is used to determine how specific an action is,
|
|
* The lower the number, the more specific an action is, and the bigger
|
|
* the chance of selecting that action when multiple actions are possible
|
|
*/
|
|
var g_UnitActions =
|
|
{
|
|
"move":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "walk",
|
|
"entities": selection,
|
|
"x": position.x,
|
|
"z": position.z,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getDefault()
|
|
});
|
|
|
|
DrawTargetMarker(position);
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_walk",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.unitAI)
|
|
return false;
|
|
return { "possible": true };
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.move") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("move", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "move",
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 12,
|
|
},
|
|
|
|
"attack-move":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
let targetClasses;
|
|
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
|
|
targetClasses = { "attack": ["Unit"] };
|
|
else
|
|
targetClasses = { "attack": ["Unit", "Structure"] };
|
|
|
|
Engine.PostNetworkCommand({
|
|
"type": "attack-walk",
|
|
"entities": selection,
|
|
"x": position.x,
|
|
"z": position.z,
|
|
"targetClasses": targetClasses,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
DrawTargetMarker(position);
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_attack_move",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.unitAI)
|
|
return false;
|
|
return { "possible": true };
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return isAttackMovePressed() &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("attack-move", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "attack-move",
|
|
"cursor": "action-attack-move",
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 30,
|
|
},
|
|
|
|
"capture":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "attack",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"allowCapture": true,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_attack",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.attack || !targetState || !targetState.capturePoints)
|
|
return false;
|
|
|
|
return {
|
|
"possible": Engine.GuiInterfaceCall("CanAttack", {
|
|
"entity": entState.id,
|
|
"target": targetState.id,
|
|
"types": ["Capture"]
|
|
})
|
|
};
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.capture") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("capture", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "capture",
|
|
"cursor": "action-capture",
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 10,
|
|
},
|
|
|
|
"attack":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "attack",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"allowCapture": false,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_attack",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.attack || !targetState || !targetState.hitpoints)
|
|
return false;
|
|
|
|
return {
|
|
"possible": Engine.GuiInterfaceCall("CanAttack", {
|
|
"entity": entState.id,
|
|
"target": targetState.id,
|
|
"types": ["!Capture"]
|
|
})
|
|
};
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.attack") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("attack", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "attack",
|
|
"cursor": "action-attack",
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 9,
|
|
},
|
|
|
|
"call-to-arms": {
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
let targetClasses;
|
|
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
|
|
targetClasses = { "attack": ["Unit"] };
|
|
else
|
|
targetClasses = { "attack": ["Unit", "Structure"] };
|
|
Engine.PostNetworkCommand({
|
|
"type": "call-to-arms",
|
|
"entities": selection,
|
|
"position": position,
|
|
"targetClasses": targetClasses,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"allowCapture": Engine.HotkeyIsPressed("session.capture"),
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
return { "possible": !!entState.unitAI };
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("call-to-arms", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "call-to-arms",
|
|
"cursor": "action-attack",
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.calltoarms") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"preSelectedActionCheck": function(target, selection)
|
|
{
|
|
return preSelectedAction == ACTION_CALLTOARMS &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"specificness": 50,
|
|
},
|
|
|
|
"patrol":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "patrol",
|
|
"entities": selection,
|
|
"x": position.x,
|
|
"z": position.z,
|
|
"target": action.target,
|
|
"targetClasses": { "attack": g_PatrolTargets },
|
|
"queued": queued,
|
|
"allowCapture": Engine.HotkeyIsPressed("session.capture"),
|
|
"formation": g_AutoFormation.getDefault()
|
|
});
|
|
|
|
DrawTargetMarker(position);
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_patrol",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.unitAI || !entState.unitAI.canPatrol)
|
|
return false;
|
|
|
|
return { "possible": true };
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.patrol") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"preSelectedActionCheck": function(target, selection)
|
|
{
|
|
return preSelectedAction == ACTION_PATROL &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("patrol", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "patrol",
|
|
"cursor": "action-patrol",
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 37,
|
|
},
|
|
|
|
"heal":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "heal",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_heal",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.heal || !targetState ||
|
|
!hasClass(targetState, "Unit") || !targetState.needsHeal ||
|
|
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
|
|
entState.id == targetState.id) // Healers can't heal themselves.
|
|
return false;
|
|
|
|
const unhealableClasses = entState.heal.unhealableClasses;
|
|
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
|
|
return false;
|
|
|
|
const healableClasses = entState.heal.healableClasses;
|
|
if (!MatchesClassList(targetState.identity.classes, healableClasses))
|
|
return false;
|
|
|
|
return { "possible": true };
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("heal", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "heal",
|
|
"cursor": "action-heal",
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 7,
|
|
},
|
|
|
|
// "Fake" action to check if an entity can be ordered to "construct"
|
|
// which is handled differently from repair as the target does not exist.
|
|
"construct":
|
|
{
|
|
"preSelectedActionCheck": function(target, selection)
|
|
{
|
|
const state = GetEntityState(selection[0]);
|
|
if (state && state.builder &&
|
|
target && target.constructor && target.constructor.name == "PlacementSupport")
|
|
return { "type": "construct" };
|
|
return false;
|
|
},
|
|
"specificness": 0,
|
|
},
|
|
|
|
"repair":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "repair",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"autocontinue": true,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": action.foundation ? "order_build" : "order_repair",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.builder || !targetState ||
|
|
!targetState.needsRepair && !targetState.foundation ||
|
|
!playerCheck(entState, targetState, ["Player", "Ally"]))
|
|
return false;
|
|
|
|
return {
|
|
"possible": true,
|
|
"foundation": targetState.foundation
|
|
};
|
|
},
|
|
"preSelectedActionCheck": function(target, selection)
|
|
{
|
|
return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || {
|
|
"type": "none",
|
|
"cursor": "action-repair-disabled",
|
|
"target": null
|
|
});
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.repair") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("repair", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "repair",
|
|
"cursor": "action-repair",
|
|
"target": target,
|
|
"foundation": actionInfo.foundation,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 11,
|
|
},
|
|
|
|
"gather":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "gather",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_gather",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.resourceGatherRates ||
|
|
!targetState || !targetState.resourceSupply)
|
|
return false;
|
|
|
|
let resource;
|
|
if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific])
|
|
resource = targetState.resourceSupply.type.specific;
|
|
else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic])
|
|
resource = targetState.resourceSupply.type.generic;
|
|
if (!resource)
|
|
return false;
|
|
|
|
return {
|
|
"possible": true,
|
|
"cursor": "action-gather-" + resource
|
|
};
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("gather", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "gather",
|
|
"cursor": actionInfo.cursor,
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 1,
|
|
},
|
|
|
|
"returnresource":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "returnresource",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_gather",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!targetState || !targetState.resourceDropsite)
|
|
return false;
|
|
|
|
const playerState = GetSimState().players[entState.player];
|
|
if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
|
|
{
|
|
if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
|
|
return false;
|
|
}
|
|
else if (!playerCheck(entState, targetState, ["Player"]))
|
|
return false;
|
|
|
|
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
|
|
return false;
|
|
|
|
const carriedType = entState.resourceCarrying[0].type;
|
|
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
|
|
return false;
|
|
|
|
return {
|
|
"possible": true,
|
|
"cursor": "action-return-" + carriedType
|
|
};
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("returnresource", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "returnresource",
|
|
"cursor": actionInfo.cursor,
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 2,
|
|
},
|
|
|
|
"cancel-setup-trade-route":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "cancel-setup-trade-route",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
|
|
playerCheck(entState, targetState, ["Enemy"]) ||
|
|
!(targetState.market.land && hasClass(entState, "Organic") ||
|
|
targetState.market.naval && hasClass(entState, "Ship")))
|
|
return false;
|
|
|
|
const tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
|
|
"trader": entState.id,
|
|
"target": targetState.id
|
|
});
|
|
|
|
if (!tradingDetails || !tradingDetails.type)
|
|
return false;
|
|
|
|
if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets)
|
|
return {
|
|
"possible": true,
|
|
"tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.")
|
|
};
|
|
return false;
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("cancel-setup-trade-route", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "cancel-setup-trade-route",
|
|
"cursor": "action-cancel-setup-trade-route",
|
|
"tooltip": actionInfo.tooltip,
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 2,
|
|
},
|
|
|
|
"setup-trade-route":
|
|
{
|
|
"execute": function(position, action, selection, queued)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "setup-trade-route",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"source": null,
|
|
"route": null,
|
|
"queued": queued,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_trade",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
|
|
playerCheck(entState, targetState, ["Enemy"]) ||
|
|
!(targetState.market.land && hasClass(entState, "Organic") ||
|
|
targetState.market.naval && hasClass(entState, "Ship")))
|
|
return false;
|
|
|
|
const tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
|
|
"trader": entState.id,
|
|
"target": targetState.id
|
|
});
|
|
|
|
if (!tradingDetails)
|
|
return false;
|
|
|
|
let tooltip;
|
|
switch (tradingDetails.type)
|
|
{
|
|
case "is first":
|
|
tooltip = translate("Origin trade market.") + "\n";
|
|
if (tradingDetails.hasBothMarkets)
|
|
tooltip += sprintf(translate("Gain: %(gain)s"), {
|
|
"gain": getTradingTooltip(tradingDetails.gain)
|
|
});
|
|
else
|
|
return false;
|
|
break;
|
|
|
|
case "is second":
|
|
tooltip = translate("Destination trade market.") + "\n" +
|
|
sprintf(translate("Gain: %(gain)s"), {
|
|
"gain": getTradingTooltip(tradingDetails.gain)
|
|
});
|
|
break;
|
|
|
|
case "set first":
|
|
tooltip = translate("Right-click to set as origin trade market");
|
|
break;
|
|
|
|
case "set second":
|
|
if (tradingDetails.gain.traderGain == 0)
|
|
return {
|
|
"possible": true,
|
|
"tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags),
|
|
"disabled": true
|
|
};
|
|
|
|
tooltip = translate("Right-click to set as destination trade market.") + "\n" +
|
|
sprintf(translate("Gain: %(gain)s"), {
|
|
"gain": getTradingTooltip(tradingDetails.gain)
|
|
});
|
|
break;
|
|
default:
|
|
error("Unknown type for tradingDetails: " + tradingDetails.type);
|
|
}
|
|
|
|
return {
|
|
"possible": true,
|
|
"tooltip": tooltip
|
|
};
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("setup-trade-route", target, selection);
|
|
if (actionInfo.disabled)
|
|
return {
|
|
"type": "none",
|
|
"cursor": "action-setup-trade-route-disabled",
|
|
"target": null,
|
|
"tooltip": actionInfo.tooltip
|
|
};
|
|
|
|
return actionInfo.possible && {
|
|
"type": "setup-trade-route",
|
|
"cursor": "action-setup-trade-route",
|
|
"tooltip": actionInfo.tooltip,
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 0,
|
|
},
|
|
|
|
"occupy-turret":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "occupy-turret",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_garrison",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.turretable || !targetState || !targetState.turretHolder ||
|
|
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
|
|
return false;
|
|
|
|
if (!targetState.turretHolder.turretPoints.find(point =>
|
|
!point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
|
|
return false;
|
|
|
|
const occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
|
|
let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
|
|
"occupied": occupiedTurrets.length,
|
|
"capacity": targetState.turretHolder.turretPoints.length
|
|
});
|
|
|
|
if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length)
|
|
tooltip = coloredText(tooltip, "orange");
|
|
|
|
return {
|
|
"possible": true,
|
|
"tooltip": tooltip
|
|
};
|
|
},
|
|
"preSelectedActionCheck": function(target, selection)
|
|
{
|
|
return preSelectedAction == ACTION_OCCUPY_TURRET && (this.actionCheck(target, selection) || {
|
|
"type": "none",
|
|
"cursor": "action-occupy-turret-disabled",
|
|
"target": null
|
|
});
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.occupyturret") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("occupy-turret", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "occupy-turret",
|
|
"cursor": "action-occupy-turret",
|
|
"tooltip": actionInfo.tooltip,
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 21,
|
|
},
|
|
|
|
"garrison":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "garrison",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_garrison",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.garrisonable || !targetState || !targetState.garrisonHolder ||
|
|
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
|
|
return false;
|
|
|
|
let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
|
|
"garrisoned": targetState.garrisonHolder.occupiedSlots,
|
|
"capacity": targetState.garrisonHolder.capacity
|
|
});
|
|
|
|
let extraCount = entState.garrisonable.size;
|
|
if (entState.garrisonHolder)
|
|
extraCount += entState.garrisonHolder.occupiedSlots;
|
|
|
|
if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity)
|
|
tooltip = coloredText(tooltip, "orange");
|
|
|
|
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
|
|
return false;
|
|
|
|
return {
|
|
"possible": true,
|
|
"tooltip": tooltip
|
|
};
|
|
},
|
|
"preSelectedActionCheck": function(target, selection)
|
|
{
|
|
return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || {
|
|
"type": "none",
|
|
"cursor": "action-garrison-disabled",
|
|
"target": null
|
|
});
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.garrison") &&
|
|
this.actionCheck(target, selection);
|
|
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("garrison", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "garrison",
|
|
"cursor": "action-garrison",
|
|
"tooltip": actionInfo.tooltip,
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 20,
|
|
},
|
|
|
|
"guard":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "guard",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_guard",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!targetState || !targetState.guard || entState.id == targetState.id ||
|
|
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
|
|
!entState.unitAI || !entState.unitAI.canGuard)
|
|
return false;
|
|
|
|
return { "possible": true };
|
|
},
|
|
"preSelectedActionCheck": function(target, selection)
|
|
{
|
|
return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || {
|
|
"type": "none",
|
|
"cursor": "action-guard-disabled",
|
|
"target": null
|
|
});
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.guard") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("guard", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "guard",
|
|
"cursor": "action-guard",
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 40,
|
|
},
|
|
|
|
"collect-treasure":
|
|
{
|
|
"execute": function(position, action, selection, queued)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "collect-treasure",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"formation": g_AutoFormation.getNull()
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_collect_treasure",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.treasureCollector ||
|
|
!targetState || !targetState.treasure)
|
|
return false;
|
|
|
|
return {
|
|
"possible": true,
|
|
"cursor": "action-collect-treasure"
|
|
};
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("collect-treasure", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "collect-treasure",
|
|
"cursor": actionInfo.cursor,
|
|
"target": target,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 1,
|
|
},
|
|
|
|
"remove-guard":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "remove-guard",
|
|
"entities": selection,
|
|
"target": action.target,
|
|
"queued": queued,
|
|
"pushFront": pushFront
|
|
});
|
|
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "order_guard",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.unitAI || !entState.unitAI.isGuarding)
|
|
return false;
|
|
return { "possible": true };
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
return Engine.HotkeyIsPressed("session.guard") &&
|
|
this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("remove-guard", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "remove-guard",
|
|
"cursor": "action-remove-guard",
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 41,
|
|
},
|
|
|
|
"focus-fire":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "focus-fire",
|
|
"entities": selection,
|
|
"x": position.x,
|
|
"z": position.z,
|
|
"target": action.target,
|
|
"data": action.data,
|
|
"queued": queued,
|
|
"pushFront": pushFront
|
|
});
|
|
|
|
if (action.data.sound)
|
|
Engine.GuiInterfaceCall("PlaySound", {
|
|
"name": "focus_fire",
|
|
"entity": action.firstAbleEntity
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.rallyPoint)
|
|
return undefined;
|
|
|
|
let tooltip;
|
|
const data = { "command": "walk" };
|
|
data.sound = false;
|
|
|
|
if (entState.attack && Engine.HotkeyIsPressed("session.focusfire"))
|
|
{
|
|
data.command = "attack-only";
|
|
if (targetState && playerCheck(entState, targetState, ["Enemy"]) && !targetState.resourceSupply)
|
|
{
|
|
data.target = targetState.id;
|
|
data.sound = true;
|
|
}
|
|
return {
|
|
"possible": true,
|
|
"data": data,
|
|
"position": targetState && targetState.position,
|
|
"cursor": "action-target",
|
|
"tooltip": tooltip
|
|
};
|
|
}
|
|
return undefined;
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
// Hotkeys are checked in the actionInfo.
|
|
return this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
// We want commands to units take precedence.
|
|
if (selection.some(ent =>
|
|
{
|
|
const entState = GetEntityState(ent);
|
|
return entState && !!entState.unitAI;
|
|
}))
|
|
return false;
|
|
|
|
const actionInfo = getActionInfo("focus-fire", target, selection);
|
|
|
|
return actionInfo.possible && {
|
|
"type": "focus-fire",
|
|
"cursor": actionInfo.cursor,
|
|
"data": actionInfo.data,
|
|
"target": target,
|
|
"tooltip": actionInfo.tooltip,
|
|
"position": actionInfo.position,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 6,
|
|
},
|
|
|
|
"set-rallypoint":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
// if there is a position set in the action then use this so that when setting a
|
|
// rally point on an entity it is centered on that entity
|
|
if (action.position)
|
|
position = action.position;
|
|
|
|
Engine.PostNetworkCommand({
|
|
"type": "set-rallypoint",
|
|
"structures": selection,
|
|
"x": position.x,
|
|
"z": position.z,
|
|
"data": action.data,
|
|
"queued": queued
|
|
});
|
|
|
|
Engine.PlayUISound(g_RallySound, false);
|
|
|
|
// Display rally point at the new coordinates, to avoid display lag
|
|
Engine.GuiInterfaceCall("DisplayRallyPoint", {
|
|
"entities": selection,
|
|
"x": position.x,
|
|
"z": position.z,
|
|
"queued": queued
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!entState.rallyPoint)
|
|
return false;
|
|
|
|
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
|
|
// except if the autorallypoint hotkey is pressed and the target can produce entities.
|
|
if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") ||
|
|
!targetState.trainer ||
|
|
!targetState.trainer.entities.length))
|
|
for (const ent of g_Selection.toList())
|
|
if (targetState.id == ent)
|
|
return false;
|
|
|
|
let tooltip;
|
|
let disabled = false;
|
|
// default to walking there (or attack-walking if hotkey pressed)
|
|
const data = { "command": "walk" };
|
|
let cursor = "";
|
|
|
|
if (isAttackMovePressed())
|
|
{
|
|
let targetClasses;
|
|
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
|
|
targetClasses = { "attack": ["Unit"] };
|
|
else
|
|
targetClasses = { "attack": ["Unit", "Structure"] };
|
|
|
|
data.command = "attack-walk";
|
|
data.targetClasses = targetClasses;
|
|
cursor = "action-attack-move";
|
|
}
|
|
|
|
if (Engine.HotkeyIsPressed("session.repair") && targetState &&
|
|
(targetState.needsRepair || targetState.foundation) &&
|
|
playerCheck(entState, targetState, ["Player", "Ally"]))
|
|
{
|
|
data.command = "repair";
|
|
data.target = targetState.id;
|
|
cursor = "action-repair";
|
|
}
|
|
else if (targetState && targetState.garrisonHolder &&
|
|
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
|
|
{
|
|
data.command = "garrison";
|
|
data.target = targetState.id;
|
|
cursor = "action-garrison";
|
|
|
|
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
|
|
"garrisoned": targetState.garrisonHolder.occupiedSlots,
|
|
"capacity": targetState.garrisonHolder.capacity
|
|
});
|
|
|
|
if (targetState.garrisonHolder.occupiedSlots >=
|
|
targetState.garrisonHolder.capacity)
|
|
tooltip = coloredText(tooltip, "orange");
|
|
}
|
|
else if (targetState && targetState.turretHolder &&
|
|
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
|
|
{
|
|
data.command = "occupy-turret";
|
|
data.target = targetState.id;
|
|
cursor = "action-garrison";
|
|
|
|
const occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
|
|
tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
|
|
"occupied": occupiedTurrets.length,
|
|
"capacity": targetState.turretHolder.turretPoints.length
|
|
});
|
|
|
|
if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length)
|
|
tooltip = coloredText(tooltip, "orange");
|
|
}
|
|
else if (targetState && targetState.resourceSupply)
|
|
{
|
|
const resourceType = targetState.resourceSupply.type;
|
|
cursor = "action-gather-" + resourceType.specific;
|
|
|
|
data.command = "gather-near-position";
|
|
data.resourceType = resourceType;
|
|
data.resourceTemplate = targetState.template;
|
|
if (!targetState.speed)
|
|
{
|
|
data.command = "gather";
|
|
data.target = targetState.id;
|
|
}
|
|
}
|
|
else if (targetState && targetState.treasure)
|
|
{
|
|
cursor = "action-collect-treasure";
|
|
data.command = "collect-treasure-near-position";
|
|
if (!targetState.speed)
|
|
{
|
|
data.command = "collect-treasure";
|
|
data.target = targetState.id;
|
|
}
|
|
}
|
|
else if (entState.market && targetState && targetState.market &&
|
|
entState.id != targetState.id &&
|
|
(!entState.market.naval || targetState.market.naval) &&
|
|
!playerCheck(entState, targetState, ["Enemy"]))
|
|
{
|
|
// Find a trader (if any) that this structure can train.
|
|
let trader;
|
|
if (entState.trainer?.entities?.length)
|
|
for (let i = 0; i < entState.trainer.entities.length; ++i)
|
|
if ((trader = GetTemplateData(entState.trainer.entities[i]).trader))
|
|
break;
|
|
|
|
const traderData = {
|
|
"firstMarket": entState.id,
|
|
"secondMarket": targetState.id,
|
|
"template": trader
|
|
};
|
|
|
|
const gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
|
|
if (gain)
|
|
{
|
|
data.command = "trade";
|
|
data.target = traderData.secondMarket;
|
|
data.source = traderData.firstMarket;
|
|
cursor = "action-setup-trade-route";
|
|
|
|
if (gain.traderGain)
|
|
tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
|
|
sprintf(
|
|
trader ?
|
|
translate("Gain: %(gain)s") :
|
|
translate("Expected gain: %(gain)s"),
|
|
{ "gain": getTradingTooltip(gain) });
|
|
else
|
|
{
|
|
disabled = true;
|
|
tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags);
|
|
cursor = "action-setup-trade-route-disabled";
|
|
}
|
|
}
|
|
}
|
|
else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
|
|
{
|
|
data.command = "repair";
|
|
data.target = targetState.id;
|
|
cursor = "action-repair";
|
|
}
|
|
else if (targetState && playerCheck(entState, targetState, ["Enemy"]))
|
|
{
|
|
data.target = targetState.id;
|
|
data.command = "attack";
|
|
if (targetState.hitpoints)
|
|
cursor = "action-attack";
|
|
else if (targetState.capturePoints)
|
|
{
|
|
cursor = "action-capture";
|
|
data.allowCapture = true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
return {
|
|
"possible": true,
|
|
"data": data,
|
|
"position": targetState && targetState.position,
|
|
"cursor": cursor,
|
|
"disabled": disabled,
|
|
"tooltip": tooltip
|
|
};
|
|
|
|
},
|
|
"hotkeyActionCheck": function(target, selection)
|
|
{
|
|
// Hotkeys are checked in the actionInfo.
|
|
return this.actionCheck(target, selection);
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
// We want commands to units take precedence.
|
|
if (selection.some(ent =>
|
|
{
|
|
const entState = GetEntityState(ent);
|
|
return entState && !!entState.unitAI;
|
|
}))
|
|
return false;
|
|
|
|
const actionInfo = getActionInfo("set-rallypoint", target, selection);
|
|
if (actionInfo.disabled)
|
|
return {
|
|
"type": "none",
|
|
"cursor": actionInfo.cursor,
|
|
"target": null,
|
|
"tooltip": actionInfo.tooltip
|
|
};
|
|
|
|
return actionInfo.possible && {
|
|
"type": "set-rallypoint",
|
|
"cursor": actionInfo.cursor,
|
|
"data": actionInfo.data,
|
|
"tooltip": actionInfo.tooltip,
|
|
"position": actionInfo.position,
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 6,
|
|
},
|
|
|
|
"unset-rallypoint":
|
|
{
|
|
"execute": function(position, action, selection, queued, pushFront)
|
|
{
|
|
Engine.PostNetworkCommand({
|
|
"type": "unset-rallypoint",
|
|
"structures": selection
|
|
});
|
|
|
|
// Remove displayed rally point
|
|
Engine.GuiInterfaceCall("DisplayRallyPoint", {
|
|
"entities": []
|
|
});
|
|
|
|
return true;
|
|
},
|
|
"getActionInfo": function(entState, targetState)
|
|
{
|
|
if (!targetState ||
|
|
entState.id != targetState.id || entState.unitAI ||
|
|
!entState.rallyPoint || !entState.rallyPoint.position)
|
|
return false;
|
|
|
|
return { "possible": true };
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
const actionInfo = getActionInfo("unset-rallypoint", target, selection);
|
|
return actionInfo.possible && {
|
|
"type": "unset-rallypoint",
|
|
"cursor": "action-unset-rally",
|
|
"firstAbleEntity": actionInfo.entity
|
|
};
|
|
},
|
|
"specificness": 11,
|
|
},
|
|
|
|
// This is a "fake" action to show a failure cursor
|
|
// when only uncontrollable entities are selected.
|
|
"uncontrollable":
|
|
{
|
|
"execute": function(position, action, selection, queued)
|
|
{
|
|
return true;
|
|
},
|
|
"actionCheck": function(target, selection)
|
|
{
|
|
// Only show this action if all entities are marked uncontrollable.
|
|
const playerState = g_SimState.players[g_ViewedPlayer];
|
|
if (playerState && playerState.controlsAll || selection.some(ent =>
|
|
{
|
|
const entState = GetEntityState(ent);
|
|
return entState && entState.identity && entState.identity.controllable;
|
|
}))
|
|
return false;
|
|
|
|
return {
|
|
"type": "none",
|
|
"cursor": "cursor-no",
|
|
"tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length)
|
|
};
|
|
},
|
|
"specificness": 100,
|
|
},
|
|
|
|
"none":
|
|
{
|
|
"execute": function(position, action, selection, queued)
|
|
{
|
|
return true;
|
|
},
|
|
"specificness": 100,
|
|
},
|
|
};
|
|
|
|
var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness);
|
|
|
|
/**
|
|
* Info and actions for the entity commands
|
|
* Currently displayed in the bottom of the central panel
|
|
*/
|
|
var g_EntityCommands =
|
|
{
|
|
"unload-all": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
let count = 0;
|
|
for (const entState of entStates)
|
|
{
|
|
if (!entState.garrisonHolder)
|
|
continue;
|
|
|
|
if (allowedPlayersCheck([entState], ["Player"]))
|
|
count += entState.garrisonHolder.entities.length;
|
|
else
|
|
for (const entity of entState.garrisonHolder.entities)
|
|
if (allowedPlayersCheck([GetEntityState(entity)], ["Player"]))
|
|
++count;
|
|
}
|
|
|
|
if (!count)
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
|
|
translate("Unload All") + "\n" +
|
|
bodyFont(translate("Order all units to leave the selected entities.")),
|
|
"icon": "garrison-out.png",
|
|
"count": count,
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
unloadAll();
|
|
},
|
|
"allowedPlayers": ["Player", "Ally"]
|
|
},
|
|
|
|
"unload-all-turrets": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
let count = 0;
|
|
for (const entState of entStates)
|
|
{
|
|
if (!entState.turretHolder)
|
|
continue;
|
|
|
|
if (allowedPlayersCheck([entState], ["Player"]))
|
|
count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity && turretPoint.ejectable).length;
|
|
else
|
|
for (const turretPoint of entState.turretHolder.turretPoints)
|
|
if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"]))
|
|
++count;
|
|
}
|
|
|
|
if (!count)
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") +
|
|
translate("Unload Turrets") + "\n" +
|
|
bodyFont(translate("Order all units to leave the selected turret points.")),
|
|
"icon": "garrison-out.png",
|
|
"count": count,
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
unloadAllTurrets();
|
|
},
|
|
"allowedPlayers": ["Player", "Ally"]
|
|
},
|
|
|
|
"delete": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
return entStates.some(entState => !isUndeletable(entState)) ?
|
|
{
|
|
"tooltip":
|
|
colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
|
|
translate("Self-Destruct") + "\n" +
|
|
bodyFont(translate("Destroy the selected entities.") + "\n" +
|
|
colorizeHotkey(
|
|
translate("Use %(hotkey)s to avoid the confirmation dialog."),
|
|
"session.noconfirmation"
|
|
)
|
|
),
|
|
"icon": "kill_small.png",
|
|
"enabled": true
|
|
} :
|
|
{
|
|
// Get all delete reasons and remove duplications
|
|
"tooltip": entStates.map(entState => isUndeletable(entState))
|
|
.filter((reason, pos, self) =>
|
|
self.indexOf(reason) == pos && reason
|
|
).join("\n"),
|
|
"icon": "kill_small_disabled.png",
|
|
"enabled": false
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
const entityIDs = entStates.reduce(
|
|
(ids, entState) =>
|
|
{
|
|
if (!isUndeletable(entState))
|
|
ids.push(entState.id);
|
|
return ids;
|
|
},
|
|
[]);
|
|
|
|
if (!entityIDs.length)
|
|
return;
|
|
|
|
const deleteSelection = () => Engine.PostNetworkCommand({
|
|
"type": "delete-entities",
|
|
"entities": entityIDs
|
|
});
|
|
|
|
if (Engine.HotkeyIsPressed("session.noconfirmation"))
|
|
deleteSelection();
|
|
else
|
|
(new DeleteSelectionConfirmation(deleteSelection)).display();
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"stop": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
// Don't show generic option to abort if not applicable or in case of guard, which has its own abort action
|
|
if (entStates.every(entState => !entState.unitAI || entState.unitAI.isIdle || entState.unitAI.isGuarding))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
|
|
translate("Abort") + "\n" +
|
|
bodyFont(translate("Cancel the current orders for the selected units.")),
|
|
"icon": "stop.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
if (entStates.length)
|
|
stopUnits(entStates.map(entState => entState.id));
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"call-to-arms": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
const classes = ["Soldier", "Warship", "Siege", "Healer"];
|
|
if (entStates.every(entState => !MatchesClassList(entState.identity.classes, classes)))
|
|
return false;
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.calltoarms") +
|
|
translate("Attack") + "\n" +
|
|
bodyFont(translate("Send the selected units on attack move to the specified location after dropping resources.")),
|
|
"icon": "call-to-arms.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
inputState = INPUT_PRESELECTEDACTION;
|
|
preSelectedAction = ACTION_CALLTOARMS;
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"garrison": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.garrisonable ||
|
|
entState.garrisonable.holder != INVALID_ENTITY))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
|
|
translate("Garrison") + "\n" +
|
|
bodyFont(translate("Order the selected units to garrison in a structure or another unit.")),
|
|
"icon": "garrison.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
inputState = INPUT_PRESELECTEDACTION;
|
|
preSelectedAction = ACTION_GARRISON;
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"occupy-turret": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.turretable ||
|
|
entState.turretable.holder != INVALID_ENTITY))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.occupyturret") +
|
|
translate("Occupy Turret") + "\n" +
|
|
bodyFont(translate("Order the selected units to occupy a turret point.")),
|
|
"icon": "occupy-turret.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
inputState = INPUT_PRESELECTEDACTION;
|
|
preSelectedAction = ACTION_OCCUPY_TURRET;
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"leave-turret": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.turretable ||
|
|
entState.turretable.holder == INVALID_ENTITY ||
|
|
!entState.turretable.ejectable))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": translate("Unload") + "\n" +
|
|
bodyFont(translate("Order the selected units to leave their turret points.")),
|
|
"icon": "leave-turret.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
leaveTurretPoints();
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"repair": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.builder))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
|
|
translate("Repair") + "\n" +
|
|
bodyFont(translate("Order the selected units to repair a structure, ship, or siege engine.")),
|
|
"icon": "repair.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
inputState = INPUT_PRESELECTEDACTION;
|
|
preSelectedAction = ACTION_REPAIR;
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"focus-rally": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.rallyPoint))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
|
|
translate("Focus on Rally Point") + "\n" +
|
|
bodyFont(translate("Center the view on the selected rally point.")),
|
|
"icon": "focus-rally.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
|
|
let focusTarget;
|
|
for (const entState of entStates)
|
|
if (entState.rallyPoint && entState.rallyPoint.position)
|
|
{
|
|
focusTarget = entState.rallyPoint.position;
|
|
break;
|
|
}
|
|
if (!focusTarget)
|
|
for (const entState of entStates)
|
|
if (entState.position)
|
|
{
|
|
focusTarget = entState.position;
|
|
break;
|
|
}
|
|
|
|
if (focusTarget)
|
|
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
|
|
},
|
|
"allowedPlayers": ["Player", "Observer"]
|
|
},
|
|
|
|
"back-to-work": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
|
|
translate("Back to Work") + "\n" +
|
|
bodyFont(translate("Order the selected units to resume their work.")),
|
|
"icon": "back-to-work.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
backToWork();
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"add-guard": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState =>
|
|
!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
|
|
translate("Guard") + "\n" +
|
|
bodyFont(translate("Order the selected units to guard a structure or unit.")),
|
|
"icon": "add-guard.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
inputState = INPUT_PRESELECTEDACTION;
|
|
preSelectedAction = ACTION_GUARD;
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"remove-guard": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": translate("Abort Guard") + "\n" +
|
|
bodyFont(translate("Order the selected units to stop guarding.")),
|
|
"icon": "remove-guard.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
removeGuard();
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"select-trading-goods": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.market))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.gui.barter.toggle") +
|
|
translate("Barter & Trade") + "\n" +
|
|
bodyFont(translate("Open the dialog for managing resource trading and bartering.")),
|
|
"icon": "economics.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
g_TradeDialog.toggle();
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"patrol": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
|
|
return false;
|
|
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
|
|
translate("Patrol") + "\n" +
|
|
bodyFont(translate("Order to repeatedly go to a point and come back, attacking all enemies along the way.")),
|
|
"icon": "patrol.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function()
|
|
{
|
|
inputState = INPUT_PRESELECTEDACTION;
|
|
preSelectedAction = ACTION_PATROL;
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"share-dropsite": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
const sharableEntities = entStates.filter(
|
|
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
|
|
if (!sharableEntities.length)
|
|
return false;
|
|
|
|
// Returns if none of the entities belong to a player with a mutual ally.
|
|
if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
|
|
(isAlly, playerId) => isAlly && playerId != entState.player)))
|
|
return false;
|
|
|
|
return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
|
|
{
|
|
"tooltip": translate("Share Dropsite") + "\n" +
|
|
bodyFont(translate("Allow allies to use this dropsite, now locked.")),
|
|
"icon": "locked_small.png",
|
|
"enabled": true
|
|
} :
|
|
{
|
|
"tooltip": translate("Lock Dropsite") + "\n" +
|
|
bodyFont(translate("Prevent allies from using this dropsite, now shared.")),
|
|
"icon": "unlocked_small.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
const sharableEntities = entStates.filter(
|
|
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
|
|
if (sharableEntities)
|
|
Engine.PostNetworkCommand({
|
|
"type": "set-dropsite-sharing",
|
|
"entities": sharableEntities.map(entState => entState.id),
|
|
"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
|
|
});
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"is-dropsite-shared": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
const shareableEntities = entStates.filter(
|
|
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
|
|
if (!shareableEntities.length)
|
|
return false;
|
|
|
|
const player = Engine.GetPlayerID();
|
|
const simState = GetSimState();
|
|
if (!g_IsObserver && !simState.players[player].hasSharedDropsites ||
|
|
shareableEntities.every(entState => controlsPlayer(entState.player)))
|
|
return false;
|
|
|
|
if (!shareableEntities.every(entState => entState.resourceDropsite.shared))
|
|
return {
|
|
"tooltip": translate("Locked Dropsite") + "\n" +
|
|
bodyFont(translate("The use of this dropsite is prohibited.")),
|
|
"icon": "locked_small.png",
|
|
"enabled": false
|
|
};
|
|
|
|
return {
|
|
"tooltip": translate("Shared Dropsite") + "\n" +
|
|
bodyFont(g_IsObserver ? translate("Allies are allowed to use this dropsite.") :
|
|
translate("You are allowed to use this dropsite.")),
|
|
"icon": "unlocked_small.png",
|
|
"enabled": false
|
|
};
|
|
},
|
|
"execute": function(entState)
|
|
{
|
|
// This command button is always disabled.
|
|
},
|
|
"allowedPlayers": ["Ally", "Observer"]
|
|
},
|
|
|
|
"autoqueue-on": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.trainer?.entities?.length || !entState.production || entState.production.autoqueue))
|
|
return false;
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") +
|
|
translate("Activate Auto-Queue") + "\n" +
|
|
bodyFont(translate("Activate the production auto-queue for the selected structures.")),
|
|
"icon": "autoqueue-on.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
if (entStates.length)
|
|
turnAutoQueueOn();
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
|
|
"autoqueue-off": {
|
|
"getInfo": function(entStates)
|
|
{
|
|
if (entStates.every(entState => !entState.production?.autoqueue))
|
|
return false;
|
|
return {
|
|
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") +
|
|
translate("Deactivate Auto-Queue") + "\n" +
|
|
bodyFont(translate("Deactivate the production auto-queue for the selected structures.")),
|
|
"icon": "autoqueue-off.png",
|
|
"enabled": true
|
|
};
|
|
},
|
|
"execute": function(entStates)
|
|
{
|
|
if (entStates.length)
|
|
turnAutoQueueOff();
|
|
},
|
|
"allowedPlayers": ["Player"]
|
|
},
|
|
};
|
|
|
|
function playerCheck(entState, targetState, validPlayers)
|
|
{
|
|
const playerState = GetSimState().players[entState.player];
|
|
for (const player of validPlayers)
|
|
if (player == "Gaia" && targetState.player == 0 ||
|
|
player == "Player" && targetState.player == entState.player ||
|
|
playerState["is" + player] && playerState["is" + player][targetState.player])
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks whether the entities have the right diplomatic status
|
|
* with respect to the currently active player.
|
|
* Also "Observer" can be used.
|
|
*
|
|
* @param {Object[]} entStates - An array containing the entity states to check.
|
|
* @param {string[]} validPlayers - An array containing the diplomatic statuses.
|
|
*
|
|
* @return {boolean} - Whether the currently active player is allowed.
|
|
*/
|
|
function allowedPlayersCheck(entStates, validPlayers)
|
|
{
|
|
// Assume we can only select entities from one player,
|
|
// or it does not matter (e.g. observer).
|
|
const targetState = entStates[0];
|
|
const playerState = GetSimState().players[Engine.GetPlayerID()];
|
|
|
|
return validPlayers.some(player =>
|
|
player == "Observer" && g_IsObserver ||
|
|
player == "Player" && controlsPlayer(targetState.player) ||
|
|
playerState && playerState["is" + player] && playerState["is" + player][targetState.player]);
|
|
}
|
|
|
|
function hasClass(entState, className)
|
|
{
|
|
// note: use the functions in globalscripts/Templates.js for more versatile matching
|
|
return entState.identity && entState.identity.classes.indexOf(className) != -1;
|
|
}
|
|
|
|
/**
|
|
* Keep in sync with Commands.js.
|
|
*/
|
|
function isUndeletable(entState)
|
|
{
|
|
const playerState = g_SimState.players[entState.player];
|
|
if (playerState && playerState.controlsAll)
|
|
return false;
|
|
|
|
if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
|
|
return translate("The entity has to be killed before it can be gathered from");
|
|
|
|
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
|
|
return translate("You cannot destroy this entity as you own less than half the capture points");
|
|
|
|
if (!entState.identity.canDelete)
|
|
return translate("This entity is undeletable");
|
|
|
|
return false;
|
|
}
|
|
|
|
function DrawTargetMarker(position)
|
|
{
|
|
Engine.GuiInterfaceCall("AddTargetMarker", {
|
|
"template": g_TargetMarker.move,
|
|
"x": position.x,
|
|
"z": position.z
|
|
});
|
|
}
|
|
|
|
function renderAndPlayFlare(position, playerGUID)
|
|
{
|
|
const playerID = g_PlayerAssignments[playerGUID].player;
|
|
Engine.GuiInterfaceCall("AddTargetMarker", {
|
|
"template": g_TargetMarker.map_flare,
|
|
"x": position.x,
|
|
"z": position.z,
|
|
// Set the owner to gaia if the flare was sent by an observer (to make the target marker white).
|
|
"owner": playerID != -1 ? playerID : 0
|
|
});
|
|
g_MiniMapPanel.flare(position, playerID);
|
|
Engine.PlayUISound(g_FlareSound, false);
|
|
addChatMessage({
|
|
"type": "flare",
|
|
"guid": playerGUID,
|
|
"position": position
|
|
});
|
|
}
|
|
|
|
function getCommandInfo(command, entStates)
|
|
{
|
|
return entStates && g_EntityCommands[command] &&
|
|
allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) &&
|
|
g_EntityCommands[command].getInfo(entStates);
|
|
}
|
|
|
|
function getActionInfo(action, target, selection)
|
|
{
|
|
if (!selection || !selection.length || !GetEntityState(selection[0]))
|
|
return { "possible": false };
|
|
|
|
// Look at the first targeted entity
|
|
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
|
|
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
|
|
const targetState = GetEntityState(target);
|
|
|
|
const simState = GetSimState();
|
|
const playerState = g_SimState.players[g_ViewedPlayer];
|
|
|
|
// Check if any entities in the selection can do some of the available actions.
|
|
for (const entityID of selection)
|
|
{
|
|
const entState = GetEntityState(entityID);
|
|
if (!entState)
|
|
continue;
|
|
|
|
if (playerState && !playerState.controlsAll && !entState.identity.controllable)
|
|
continue;
|
|
|
|
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
|
|
{
|
|
const r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
|
|
if (r && r.possible)
|
|
{
|
|
r.entity = entityID;
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
return { "possible": false };
|
|
}
|