Index: ps/trunk/binaries/data/mods/public/gui/session/ResearchProgress.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/ResearchProgress.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/gui/session/ResearchProgress.js (revision 25958)
@@ -1,109 +1,120 @@
/**
* This class is responsible for displaying the currently researched technologies in an overlay.
*/
class ResearchProgress
{
constructor(playerViewControl, selection)
{
this.buttons = Engine.GetGUIObjectByName("researchStartedButtons").children;
this.buttonHandlers = this.buttons.map((button, i) => new ResearchProgressButton(selection, i));
/**
* Top coordinate of the research list.
* Changes depending on the number of displayed counters.
*/
this.topOffset = 0;
let updater = this.updateResearchProgress.bind(this);
registerSimulationUpdateHandler(updater);
playerViewControl.registerViewedPlayerChangeHandler(updater);
g_OverlayCounterManager.registerResizeHandler(this.setTopOffset.bind(this));
}
setTopOffset(offset)
{
this.topOffset = offset;
}
updateResearchProgress()
{
let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer);
let i = 0;
for (let techName in researchStarted)
{
if (i == this.buttons.length)
break;
this.buttonHandlers[i++].onResearchedProgress(this.topOffset, techName, researchStarted[techName]);
}
while (i < this.buttons.length)
this.buttons[i++].hidden = true;
}
}
/**
* This is an individual button displaying a tech currently researched by the currently viewed player.
*/
class ResearchProgressButton
{
constructor(selection, i)
{
this.selection = selection;
this.button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
this.sprite = Engine.GetGUIObjectByName("researchStartedIcon[" + i + "]");
this.progress = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + i + "]");
this.timeRemaining = Engine.GetGUIObjectByName("researchStartedTimeRemaining[" + i + "]");
+ this.paused = Engine.GetGUIObjectByName("researchPausedIcon[" + i + "]");
this.buttonHeight = this.button.size.bottom - this.button.size.top;
this.buttonTop = this.Margin + (this.Margin + this.buttonHeight) * i;
this.progressHeight = this.progress.size.bottom - this.progress.size.top;
this.progressTop = this.progress.size.top;
this.button.onPress = this.onPress.bind(this);
}
onResearchedProgress(offset, techName, researchStatus)
{
this.researcher = researchStatus.researcher;
let template = GetTechnologyData(techName, g_Players[g_ViewedPlayer].civ);
- this.sprite.sprite = "stretched:" + this.PortraitDirectory + template.icon;
+ let modifier = "stretched:";
+ if (researchStatus.paused)
+ modifier += "color:0 0 0 127:grayscale:";
+ this.sprite.sprite = modifier + this.PortraitDirectory + template.icon;
let size = this.button.size;
size.top = offset + this.buttonTop;
size.bottom = size.top + this.buttonHeight;
this.button.size = size;
this.button.tooltip = getEntityNames(template);
+ if (researchStatus.paused)
+ this.button.tooltip += "\n" + translate(this.PausedResearchString);
this.button.hidden = false;
size = this.progress.size;
size.top = this.progressTop + this.progressHeight * researchStatus.progress;
this.progress.size = size;
this.timeRemaining.caption =
Engine.FormatMillisecondsIntoDateStringGMT(
researchStatus.timeRemaining,
translateWithContext("countdown format", this.CountdownFormat));
+
+ this.paused.hidden = !researchStatus.paused;
}
onPress()
{
this.selection.selectAndMoveTo(this.researcher);
}
}
/**
* Distance between consecutive buttons.
*/
ResearchProgressButton.prototype.Margin = 4;
/**
* Directory containing all icons.
*/
ResearchProgressButton.prototype.PortraitDirectory = "session/portraits/";
/**
* This format is used when displaying the remaining time of the currently viewed techs in research.
*/
ResearchProgressButton.prototype.CountdownFormat = markForTranslationWithContext("countdown format", "m:ss");
+
+// Translation: String displayed when the research is paused. E.g. by being garrisoned or when not the first item in the queue.
+ResearchProgressButton.prototype.PausedResearchString = markForTranslation("(This item is paused.)");
Index: ps/trunk/binaries/data/mods/public/gui/session/ResearchProgress.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/ResearchProgress.xml (revision 25957)
+++ ps/trunk/binaries/data/mods/public/gui/session/ResearchProgress.xml (revision 25958)
@@ -1,12 +1,13 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25958)
@@ -1,1712 +1,1716 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
const SDLK_LEFTBRACKET = 91;
const SDLK_RIGHTBRACKET = 93;
const SDLK_RSHIFT = 303;
const SDLK_LSHIFT = 304;
const SDLK_RCTRL = 305;
const SDLK_LCTRL = 306;
const SDLK_RALT = 307;
const SDLK_LALT = 308;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too.
const ACTION_NONE = 0;
const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
const ACTION_GUARD = 3;
const ACTION_PATROL = 4;
const ACTION_OCCUPY_TURRET = 5;
const ACTION_CALLTOARMS = 6;
var preSelectedAction = ACTION_NONE;
const INPUT_NORMAL = 0;
const INPUT_SELECTING = 1;
const INPUT_BANDBOXING = 2;
const INPUT_BUILDING_PLACEMENT = 3;
const INPUT_BUILDING_CLICK = 4;
const INPUT_BUILDING_DRAG = 5;
const INPUT_BATCHTRAINING = 6;
const INPUT_PRESELECTEDACTION = 7;
const INPUT_BUILDING_WALL_CLICK = 8;
const INPUT_BUILDING_WALL_PATHING = 9;
const INPUT_UNIT_POSITION_START = 10;
const INPUT_UNIT_POSITION = 11;
const INPUT_FLARE = 12;
var inputState = INPUT_NORMAL;
const INVALID_ENTITY = 0;
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
/**
* Containing the ingame position which span the line.
*/
var g_FreehandSelection_InputLine = [];
/**
* Minimum squared distance when a mouse move is called a drag.
*/
const g_FreehandSelection_ResolutionInputLineSquared = 1;
/**
* Minimum length a dragged line should have to use the freehand selection.
*/
const g_FreehandSelection_MinLengthOfLine = 8;
/**
* To start the freehandSelection function you need a minimum number of units.
* Minimum must be 2, for better performance you could set it higher.
*/
const g_FreehandSelection_MinNumberOfUnits = 2;
/**
* Number of pixels the mouse can move before the action is considered a drag.
*/
const g_MaxDragDelta = 4;
/**
* Used for remembering mouse coordinates at start of drag operations.
*/
var g_DragStart;
/**
* Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities.
* If any mousedown or mouseup of a sequence of clicks lands on a unit,
* that unit will be selected, which makes it easier to click on moving units.
*/
var clickedEntity = INVALID_ENTITY;
/**
* Store the last time the flare functionality was used to prevent overusage.
*/
var g_LastFlareTime;
/**
* The duration in ms for which we disable flaring after each flare to prevent overusage.
*/
const g_FlareCooldown = 3000;
// Same double-click behaviour for hotkey presses.
const doublePressTime = 500;
var doublePressTimer = 0;
var prevHotkey = 0;
function updateCursorAndTooltip()
{
let cursorSet = false;
let tooltipSet = false;
let informationTooltip = Engine.GetGUIObjectByName("informationTooltip");
if (inputState == INPUT_FLARE || inputState == INPUT_NORMAL && Engine.HotkeyIsPressed("session.flare") && !g_IsObserver)
{
Engine.SetCursor("action-flare");
cursorSet = true;
}
else if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap())
{
let action = determineAction(mouseX, mouseY, g_MiniMapPanel.isMouseOverMiniMap());
if (action)
{
if (action.cursor)
{
Engine.SetCursor(action.cursor);
cursorSet = true;
}
if (action.tooltip)
{
tooltipSet = true;
informationTooltip.caption = action.tooltip;
informationTooltip.hidden = false;
}
}
}
if (!cursorSet)
Engine.ResetCursor();
if (!tooltipSet)
informationTooltip.hidden = true;
let placementTooltip = Engine.GetGUIObjectByName("placementTooltip");
if (placementSupport.tooltipMessage)
placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip";
placementTooltip.caption = placementSupport.tooltipMessage || "";
placementTooltip.hidden = !placementSupport.tooltipMessage;
}
function updateBuildingPlacementPreview()
{
// The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or
// in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to.
// See onSimulationUpdate in session.js.
if (placementSupport.mode === "building")
{
if (placementSupport.template && placementSupport.position)
{
let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed
});
placementSupport.tooltipError = !result.success;
placementSupport.tooltipMessage = "";
if (!result.success)
{
if (result.message && result.parameters)
{
let message = result.message;
if (result.translateMessage)
if (result.pluralMessage)
message = translatePlural(result.message, result.pluralMessage, result.pluralCount);
else
message = translate(message);
let parameters = result.parameters;
if (result.translateParameters)
translateObjectKeys(parameters, result.translateParameters);
placementSupport.tooltipMessage = sprintf(message, parameters);
}
return false;
}
if (placementSupport.attack && placementSupport.attack.Ranged)
{
let cmd = {
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"range": placementSupport.attack.Ranged.maxRange,
"elevationBonus": placementSupport.attack.Ranged.elevationBonus
};
let averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range);
let range = Math.round(cmd.range);
placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" +
sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange });
}
return true;
}
}
else if (placementSupport.mode === "wall" &&
placementSupport.wallSet && placementSupport.position)
{
placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities(
placementSupport.wallSet.templates.tower,
placementSupport.wallSnapEntitiesIncludeOffscreen,
true, // require exact template match
true // include foundations
);
return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
"wallSet": placementSupport.wallSet,
"start": placementSupport.position,
"end": placementSupport.wallEndPosition,
"snapEntities": placementSupport.wallSnapEntities // snapping entities (towers) for starting a wall segment
});
}
return false;
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y, fromMiniMap)
{
let selection = g_Selection.toList();
if (!selection.length)
{
preSelectedAction = ACTION_NONE;
return undefined;
}
let entState = GetEntityState(selection[0]);
if (!entState)
return undefined;
if (!selection.every(ownsEntity) &&
!(g_SimState.players[g_ViewedPlayer] &&
g_SimState.players[g_ViewedPlayer].controlsAll))
return undefined;
let target;
if (!fromMiniMap)
{
let ent = Engine.PickEntityAtPoint(x, y);
if (ent != INVALID_ENTITY)
target = ent;
}
// Decide between the following ordered actions,
// if two actions are possible, the first one is taken
// thus the most specific should appear first.
if (preSelectedAction != ACTION_NONE)
{
for (let action of g_UnitActionsSortedKeys)
if (g_UnitActions[action].preSelectedActionCheck)
{
let r = g_UnitActions[action].preSelectedActionCheck(target, selection);
if (r)
return r;
}
return { "type": "none", "cursor": "", "target": target };
}
for (let action of g_UnitActionsSortedKeys)
if (g_UnitActions[action].hotkeyActionCheck)
{
let r = g_UnitActions[action].hotkeyActionCheck(target, selection);
if (r)
return r;
}
for (let action of g_UnitActionsSortedKeys)
if (g_UnitActions[action].actionCheck)
{
let r = g_UnitActions[action].actionCheck(target, selection);
if (r)
return r;
}
return { "type": "none", "cursor": "", "target": target };
}
function ownsEntity(ent)
{
let entState = GetEntityState(ent);
return entState && entState.player == g_ViewedPlayer;
}
function isAttackMovePressed()
{
return Engine.HotkeyIsPressed("session.attackmove") ||
Engine.HotkeyIsPressed("session.attackmoveUnit");
}
function isSnapToEdgesEnabled()
{
let config = Engine.ConfigDB_GetValue("user", "gui.session.snaptoedges");
let hotkeyPressed = Engine.HotkeyIsPressed("session.snaptoedges");
return hotkeyPressed == (config == "disabled");
}
function tryPlaceBuilding(queued, pushFront)
{
if (placementSupport.mode !== "building")
{
error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'");
return false;
}
if (!updateBuildingPlacementPreview())
{
Engine.GuiInterfaceCall("PlaySound", {
"name": "invalid_building_placement",
"entity": g_Selection.getFirstSelected()
});
return false;
}
let selection = Engine.HotkeyIsPressed("session.orderone") &&
popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList();
Engine.PostNetworkCommand({
"type": "construct",
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed,
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] });
if (!queued || !g_Selection.size())
placementSupport.Reset();
else
placementSupport.RandomizeActorSeed();
return true;
}
function tryPlaceWall(queued)
{
if (placementSupport.mode !== "wall")
{
error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'");
return false;
}
let wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
if (!(wallPlacementInfo === false || typeof wallPlacementInfo === "object"))
{
error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo));
return false;
}
if (!wallPlacementInfo)
return false;
let selection = Engine.HotkeyIsPressed("session.orderone") &&
popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList();
let cmd = {
"type": "construct-wall",
"autorepair": true,
"autocontinue": true,
"queued": queued,
"entities": selection,
"wallSet": placementSupport.wallSet,
"pieces": wallPlacementInfo.pieces,
"startSnappedEntity": wallPlacementInfo.startSnappedEnt,
"endSnappedEntity": wallPlacementInfo.endSnappedEnt,
"formation": g_AutoFormation.getNull()
};
// Make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end
// point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed
// (this is somewhat non-ideal and hardcode-ish).
let hasWallSegment = false;
for (let piece of cmd.pieces)
{
if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
{
hasWallSegment = true;
break;
}
}
if (hasWallSegment)
{
Engine.PostNetworkCommand(cmd);
Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] });
}
return true;
}
/**
* Updates the bandbox object with new positions and visibility.
* @returns {array} The coordinates of the vertices of the bandbox.
*/
function updateBandbox(bandbox, ev, hidden)
{
let scale = +Engine.ConfigDB_GetValue("user", "gui.scale");
let vMin = Vector2D.min(g_DragStart, ev);
let vMax = Vector2D.max(g_DragStart, ev);
bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale);
bandbox.hidden = hidden;
return [vMin.x, vMin.y, vMax.x, vMax.y];
}
// Define some useful unit filters for getPreferredEntities.
var unitFilters = {
"isUnit": entity => {
let entState = GetEntityState(entity);
return entState && hasClass(entState, "Unit");
},
"isDefensive": entity => {
let entState = GetEntityState(entity);
return entState && hasClass(entState, "Defensive");
},
"isMilitary": entity => {
let entState = GetEntityState(entity);
return entState &&
g_MilitaryTypes.some(c => hasClass(entState, c));
},
"isNonMilitary": entity => {
let entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
!g_MilitaryTypes.some(c => hasClass(entState, c));
},
"isIdle": entity => {
let entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.unitAI &&
entState.unitAI.isIdle &&
!hasClass(entState, "Domestic");
},
"isWounded": entity => {
let entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.maxHitpoints &&
100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold");
},
"isAnything": entity => {
return true;
}
};
// Choose, inside a list of entities, which ones will be selected.
// We may use several entity filters, until one returns at least one element.
function getPreferredEntities(ents)
{
let filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything];
if (Engine.HotkeyIsPressed("selection.militaryonly"))
filters = [unitFilters.isMilitary];
if (Engine.HotkeyIsPressed("selection.nonmilitaryonly"))
filters = [unitFilters.isNonMilitary];
if (Engine.HotkeyIsPressed("selection.idleonly"))
filters = [unitFilters.isIdle];
if (Engine.HotkeyIsPressed("selection.woundedonly"))
filters = [unitFilters.isWounded];
let preferredEnts = [];
for (let i = 0; i < filters.length; ++i)
{
preferredEnts = ents.filter(filters[i]);
if (preferredEnts.length)
break;
}
return preferredEnts;
}
function handleInputBeforeGui(ev, hoveredObject)
{
if (GetSimState().cinemaPlaying)
return false;
// Capture cursor position so we can use it for displaying cursors,
// and key states.
switch (ev.type)
{
case "mousebuttonup":
case "mousebuttondown":
case "mousemotion":
mouseX = ev.x;
mouseY = ev.y;
break;
}
mouseIsOverObject = (hoveredObject != null);
// Close the menu when interacting with the game world.
if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") &&
(ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT))
g_Menu.close();
// State-machine processing:
//
// (This is for states which should override the normal GUI processing - events will
// be processed here before being passed on, and propagation will stop if this function
// returns true)
//
// TODO: it'd probably be nice to have a better state-machine system, with guaranteed
// entry/exit functions, since this is a bit broken now
switch (inputState)
{
case INPUT_BANDBOXING:
let bandbox = Engine.GetGUIObjectByName("bandbox");
switch (ev.type)
{
case "mousemotion":
{
let rect = updateBandbox(bandbox, ev, false);
let ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
let preferredEntities = getPreferredEntities(ents);
g_Selection.setHighlightList(preferredEntities);
return false;
}
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
let rect = updateBandbox(bandbox, ev, true);
let ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
g_Selection.setHighlightList([]);
if (Engine.HotkeyIsPressed("selection.add"))
g_Selection.addList(ents);
else if (Engine.HotkeyIsPressed("selection.remove"))
g_Selection.removeList(ents);
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel selection.
bandbox.hidden = true;
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_UNIT_POSITION:
switch (ev.type)
{
case "mousemotion":
return positionUnitsFreehandSelectionMouseMove(ev);
case "mousebuttonup":
return positionUnitsFreehandSelectionMouseUp(ev);
}
break;
case INPUT_BUILDING_CLICK:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved far enough from the original click location,
// then switch to drag-orientation mode.
let maxDragDelta = 16;
if (g_DragStart.distanceTo(ev) >= maxDragDelta)
{
inputState = INPUT_BUILDING_DRAG;
return false;
}
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If queued, let the player continue placing another of the same building.
let queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront")))
{
if (queued && g_Selection.size())
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
inputState = INPUT_BUILDING_PLACEMENT;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_CLICK:
// User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point
// by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode.
switch (ev.type)
{
case "mousebuttonup":
if (ev.button === SDL_BUTTON_LEFT)
{
inputState = INPUT_BUILDING_WALL_PATHING;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_PATHING:
// User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
// Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
// normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
// user to continue building walls.
switch (ev.type)
{
case "mousemotion":
placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
// Update the structure placement preview, and by extension, the list of snapping candidate entities for both (!)
// the ending point and the starting point to snap to.
//
// TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
// where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
// foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
// the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
// in them. Might be useful to query only for entities within a certain range around the starting point and ending
// points.
placementSupport.wallSnapEntitiesIncludeOffscreen = true;
let result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
if (result && result.cost)
{
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost });
placementSupport.tooltipMessage = [
getEntityCostTooltip(result),
getNeededResourcesTooltip(neededResources)
].filter(tip => tip).join("\n");
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
let queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceWall(queued))
{
if (queued)
{
// Continue building, just set a new starting position where we left off.
placementSupport.position = placementSupport.wallEndPosition;
placementSupport.wallEndPosition = undefined;
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
}
}
else
placementSupport.tooltipMessage = translate("Cannot build wall here!");
updateBuildingPlacementPreview();
return true;
}
if (ev.button == SDL_BUTTON_RIGHT)
{
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_DRAG:
switch (ev.type)
{
case "mousemotion":
let maxDragDelta = 16;
if (g_DragStart.distanceTo(ev) >= maxDragDelta)
// Rotate in the direction of the cursor.
placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
else
// If the cursor is near the center, snap back to the default orientation.
placementSupport.SetDefaultAngle();
let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo(
placementSupport.position.x, placementSupport.position.z)
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
updateBuildingPlacementPreview();
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If queued, let the player continue placing another of the same structure.
let queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront")))
{
if (queued && g_Selection.size())
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
inputState = INPUT_BUILDING_PLACEMENT;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BATCHTRAINING:
if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain")
{
flushTrainingBatch();
inputState = INPUT_NORMAL;
}
break;
}
return false;
}
function handleInputAfterGui(ev)
{
if (GetSimState().cinemaPlaying)
return false;
if (ev.hotkey === undefined)
ev.hotkey = null;
if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeypress");
updateAdditionalHighlight();
}
else if (ev.hotkey == "session.highlightguarded")
{
g_ShowGuarded = (ev.type == "hotkeypress");
updateAdditionalHighlight();
}
if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING)
clickedEntity = INVALID_ENTITY;
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
let ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (Engine.HotkeyIsPressed("session.flare") && controlsPlayer(g_ViewedPlayer))
{
triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
return true;
}
if (ev.button == SDL_BUTTON_LEFT)
{
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_SELECTING;
// If a single click occured, reset the clickedEntity.
// Also set it if we're double/triple clicking and missed the unit earlier.
if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY)
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
if (!controlsPlayer(g_ViewedPlayer))
break;
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_UNIT_POSITION_START;
}
break;
case "hotkeypress":
if (ev.hotkey.indexOf("selection.group.") == 0)
{
let now = Date.now();
if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey)
{
if (ev.hotkey.indexOf("selection.group.select.") == 0)
{
let sptr = ev.hotkey.split(".");
performGroup("snap", sptr[3]);
}
}
else
{
let sptr = ev.hotkey.split(".");
performGroup(sptr[2], sptr[3]);
doublePressTimer = now;
prevHotkey = ev.hotkey;
}
}
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
case "mousemotion":
let ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
let action = determineAction(ev.x, ev.y);
if (!action)
break;
if (!Engine.HotkeyIsPressed("session.queue") && !Engine.HotkeyIsPressed("session.orderone"))
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
}
return doAction(action, ev);
}
if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
default:
// Slight hack: If selection is empty, reset the input state.
if (!g_Selection.size())
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
let ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
if (clickedEntity == INVALID_ENTITY)
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
// Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event.
if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity))
{
clickedEntity = INVALID_ENTITY;
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.reset();
resetIdleUnit();
}
inputState = INPUT_NORMAL;
return true;
}
if (Engine.GetFollowedEntity() != clickedEntity)
Engine.CameraFollow(0);
let ents = [];
if (ev.clicks == 1)
ents = [clickedEntity];
else
{
let showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
let matchRank = true;
let templateToMatch;
if (ev.clicks == 2)
{
templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName;
if (templateToMatch)
matchRank = false;
else
// No selection group name defined, so fall back to exact match.
templateToMatch = GetEntityState(clickedEntity).template;
}
else
// Triple click
// Select units matching exact template name (same rank).
templateToMatch = GetEntityState(clickedEntity).template;
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
}
if (Engine.HotkeyIsPressed("selection.add"))
g_Selection.addList(ents);
else if (Engine.HotkeyIsPressed("selection.remove"))
g_Selection.removeList(ents);
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_UNIT_POSITION_START:
switch (ev.type)
{
case "mousemotion":
if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta))
{
inputState = INPUT_UNIT_POSITION;
return false;
}
break;
case "mousebuttonup":
inputState = INPUT_NORMAL;
if (ev.button == SDL_BUTTON_RIGHT)
{
let action = determineAction(ev.x, ev.y);
if (action)
return doAction(action, ev);
}
break;
}
break;
case INPUT_BUILDING_PLACEMENT:
switch (ev.type)
{
case "mousemotion":
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (placementSupport.mode === "wall")
{
// Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is
// still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities
// itself happens in the call to updateBuildingPlacementPreview below.)
placementSupport.wallSnapEntitiesIncludeOffscreen = false;
}
else
{
if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost }))
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
if (isSnapToEdgesEnabled())
{
// We need to reset the angle before the snapping to edges,
// because we want to get the angle near to the default one.
placementSupport.SetDefaultAngle();
}
let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo(
placementSupport.position.x, placementSupport.position.z)
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
}
updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
if (placementSupport.mode === "wall")
{
let validPlacement = updateBuildingPlacementPreview();
if (validPlacement !== false)
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (isSnapToEdgesEnabled())
{
let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"snapToEdges": Engine.GetEdgesOfStaticObstructionsOnScreenNearTo(
placementSupport.position.x, placementSupport.position.z)
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
}
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_BUILDING_CLICK;
}
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building.
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
case "hotkeydown":
let rotation_step = Math.PI / 12; // 24 clicks make a full rotation
switch (ev.hotkey)
{
case "session.rotate.cw":
placementSupport.angle += rotation_step;
updateBuildingPlacementPreview();
break;
case "session.rotate.ccw":
placementSupport.angle -= rotation_step;
updateBuildingPlacementPreview();
break;
}
break;
}
break;
case INPUT_FLARE:
if (ev.type == "mousebuttondown")
{
if (ev.button == SDL_BUTTON_LEFT && controlsPlayer(g_ViewedPlayer))
{
triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
inputState = INPUT_NORMAL;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
inputState = INPUT_NORMAL;
return true;
}
}
}
return false;
}
function doAction(action, ev)
{
if (!controlsPlayer(g_ViewedPlayer))
return false;
return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action);
}
function popOneFromSelection(action)
{
// Pick the first unit that can do this order.
let unit = action.firstAbleEntity || g_Selection.find(entity =>
["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method =>
g_UnitActions[action.type][method] &&
g_UnitActions[action.type][method](action.target || undefined, [entity])
));
if (unit)
{
g_Selection.removeList([unit], true);
return [unit];
}
return null;
}
function positionUnitsFreehandSelectionMouseMove(ev)
{
// Converting the input line into a List of points.
// For better performance the points must have a minimum distance to each other.
let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
if (!g_FreehandSelection_InputLine.length ||
target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >=
g_FreehandSelection_ResolutionInputLineSquared)
g_FreehandSelection_InputLine.push(target);
return false;
}
function positionUnitsFreehandSelectionMouseUp(ev)
{
inputState = INPUT_NORMAL;
let inputLine = g_FreehandSelection_InputLine;
g_FreehandSelection_InputLine = [];
if (ev.button != SDL_BUTTON_RIGHT)
return true;
let lengthOfLine = 0;
for (let i = 1; i < inputLine.length; ++i)
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
// Checking the line for a minimum length to save performance.
if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits)
{
let action = determineAction(ev.x, ev.y);
return !!action && doAction(action, ev);
}
// Even distribution of the units on the line.
let p0 = inputLine[0];
let entityDistribution = [p0];
let distanceBetweenEnts = lengthOfLine / (selection.length - 1);
let freeDist = -distanceBetweenEnts;
for (let i = 1; i < inputLine.length; ++i)
{
let p1 = inputLine[i];
freeDist += inputLine[i - 1].distanceTo(p1);
while (freeDist >= 0)
{
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1);
entityDistribution.push(p0);
freeDist -= distanceBetweenEnts;
}
}
// Rounding errors can lead to missing or too many points.
entityDistribution = entityDistribution.slice(0, selection.length);
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
entityDistribution.reverse();
Engine.PostNetworkCommand({
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
"entities": selection,
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
"queued": Engine.HotkeyIsPressed("session.queue"),
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
"formation": NULL_FORMATION,
});
// Add target markers with a minimum distance of 5 to each other.
let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts);
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker)
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y });
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": selection[0]
});
return true;
}
function triggerFlareAction(target)
{
let now = Date.now();
if (g_LastFlareTime && now < g_LastFlareTime + g_FlareCooldown)
return;
g_LastFlareTime = now;
displayFlare(target, Engine.GetPlayerID());
Engine.PlayUISound(g_FlareSound, false);
Engine.PostNetworkCommand({
"type": "map-flare",
"target": target
});
}
function handleUnitAction(target, action)
{
if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute)
{
error("Invalid action.type " + action.type);
return false;
}
let selection = Engine.HotkeyIsPressed("session.orderone") &&
popOneFromSelection(action) || g_Selection.toList();
// If the session.queue hotkey is down, add the order to the unit's order queue instead
// of running it immediately. If the pushorderfront hotkey is down, execute the order
// immidiately and continue the rest of the queue afterwards.
return g_UnitActions[action.type].execute(
target,
action,
selection,
Engine.HotkeyIsPressed("session.queue"),
Engine.HotkeyIsPressed("session.pushorderfront"));
}
function getEntityLimitAndCount(playerState, entType)
{
let ret = {
"entLimit": undefined,
"entCount": undefined,
"entLimitChangers": undefined,
"canBeAddedCount": undefined,
"matchLimit": undefined,
"matchCount": undefined,
"type": undefined
};
if (!playerState.entityLimits)
return ret;
let template = GetTemplateData(entType);
let entCategory;
let matchLimit;
if (template.trainingRestrictions)
{
entCategory = template.trainingRestrictions.category;
matchLimit = template.trainingRestrictions.matchLimit;
ret.type = "training";
}
else if (template.buildRestrictions)
{
entCategory = template.buildRestrictions.category;
matchLimit = template.buildRestrictions.matchLimit;
ret.type = "build";
}
if (entCategory && playerState.entityLimits[entCategory] !== undefined)
{
ret.entLimit = playerState.entityLimits[entCategory] || 0;
ret.entCount = playerState.entityCounts[entCategory] || 0;
ret.entLimitChangers = playerState.entityLimitChangers[entCategory];
ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0);
}
if (matchLimit)
{
ret.matchLimit = matchLimit;
ret.matchCount = playerState.matchEntityCounts[entType] || 0;
ret.canBeAddedCount = Math.min(Math.max(ret.entLimit - ret.entCount, 0), Math.max(ret.matchLimit - ret.matchCount, 0));
}
return ret;
}
/**
* Called by GUI when user clicks construction button.
* @param {string} buildTemplate - Template name of the entity the user wants to build.
*/
function startBuildingPlacement(buildTemplate, playerState)
{
if (getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0)
return;
// TODO: we should clear any highlight selection rings here. If the cursor was over an entity before going onto the GUI
// to start building a structure, then the highlight selection rings are kept during the construction of the structure.
// Gives the impression that somehow the hovered-over entity has something to do with the structure you're building.
placementSupport.Reset();
let templateData = GetTemplateData(buildTemplate);
if (templateData.wallSet)
{
placementSupport.mode = "wall";
placementSupport.wallSet = templateData.wallSet;
inputState = INPUT_BUILDING_PLACEMENT;
}
else
{
placementSupport.mode = "building";
placementSupport.template = buildTemplate;
inputState = INPUT_BUILDING_PLACEMENT;
}
if (templateData.attack &&
templateData.attack.Ranged &&
templateData.attack.Ranged.maxRange)
placementSupport.attack = templateData.attack;
}
// Batch training:
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
// When the user releases shift, or clicks on a different training button, we create the batched units
var g_BatchTrainingEntities;
var g_BatchTrainingType;
var g_NumberOfBatches;
var g_BatchTrainingEntityAllowedCount;
var g_BatchSize = getDefaultBatchTrainingSize();
function OnTrainMouseWheel(dir)
{
if (!Engine.HotkeyIsPressed("session.batchtrain"))
return;
g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio");
if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize))
g_BatchSize = 1;
updateSelectionDetails();
}
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(entity => {
let state = GetEntityState(entity);
return state && state.production && state.production.entities.length &&
state.production.entities.indexOf(trainEntType) != -1 && (!state.upgrade || !state.upgrade.isUpgrading);
});
}
function initBatchTrain()
{
registerConfigChangeHandler(changes => {
if (changes.has("gui.session.batchtrainingsize"))
updateDefaultBatchSize();
});
}
function getDefaultBatchTrainingSize()
{
let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
return Number.isInteger(num) && num > 0 ? num : 5;
}
function getBatchTrainingSize()
{
return Math.max(Math.round(g_BatchSize), 1);
}
function updateDefaultBatchSize()
{
g_BatchSize = getDefaultBatchTrainingSize();
}
/**
* Add the unit shown at position to the training queue for all entities in the selection.
* @param {number} position - The position of the template to train.
*/
function addTrainingByPosition(position)
{
let playerState = GetSimState().players[Engine.GetPlayerID()];
let selection = g_Selection.toList();
if (!playerState || !selection.length)
return;
let trainableEnts = getAllTrainableEntitiesFromSelection();
let entToTrain = trainableEnts[position];
if (!entToTrain)
return;
addTrainingToQueue(selection, entToTrain, playerState);
}
// Called by GUI when user clicks training button
function addTrainingToQueue(selection, trainEntType, playerState)
{
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount;
let decrement = Engine.HotkeyIsPressed("selection.remove");
let template;
if (!decrement)
template = GetTemplateData(trainEntType);
// Batch training only possible if we can train at least 2 units.
if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1))
{
if (inputState == INPUT_BATCHTRAINING)
{
// Check if we are training in the same structure(s) as the last batch.
// NOTE: We just check if the arrays are the same and if the order is the same.
// If the order changed, we have a new selection and we should create a new batch.
// If we're already creating a batch of this unit (in the same structure(s)), then just extend it
// (if training limits allow).
if (g_BatchTrainingEntities.length == selection.length &&
g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) &&
g_BatchTrainingType == trainEntType)
{
if (decrement)
{
--g_NumberOfBatches;
if (g_NumberOfBatches <= 0)
inputState = INPUT_NORMAL;
}
else if (canBeAddedCount == undefined ||
canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length)
{
if (Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize())
}))
return;
++g_NumberOfBatches;
}
g_BatchTrainingEntityAllowedCount = canBeAddedCount;
return;
}
else if (!decrement)
flushTrainingBatch();
}
if (decrement || Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, getBatchTrainingSize())
}))
return;
inputState = INPUT_BATCHTRAINING;
g_BatchTrainingEntities = selection;
g_BatchTrainingType = trainEntType;
g_BatchTrainingEntityAllowedCount = canBeAddedCount;
g_NumberOfBatches = 1;
}
else
{
let buildingsForTraining = appropriateBuildings;
if (canBeAddedCount !== undefined)
buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount);
Engine.PostNetworkCommand({
"type": "train",
"template": trainEntType,
"count": 1,
- "entities": buildingsForTraining
+ "entities": buildingsForTraining,
+ "pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
}
/**
* Returns the number of units that will be present in a batch if the user clicks
* the training button depending on the batch training modifier hotkey.
*/
function getTrainingStatus(selection, trainEntType, playerState)
{
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
let nextBatchTrainingCount = 0;
let canBeAddedCount;
if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType)
{
nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize();
canBeAddedCount = g_BatchTrainingEntityAllowedCount;
}
else
canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount;
// We need to calculate count after the next increment if possible.
if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) &&
Engine.HotkeyIsPressed("session.batchtrain"))
nextBatchTrainingCount += getBatchTrainingSize();
nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1);
// If training limits don't allow us to train batchedSize in each appropriate structure,
// train as many full batches as we can and the remainder in one more structure.
let buildingsCountToTrainFullBatch = appropriateBuildings.length;
let remainderToTrain = 0;
if (canBeAddedCount !== undefined &&
canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length)
{
buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount);
remainderToTrain = canBeAddedCount % nextBatchTrainingCount;
}
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain];
}
function flushTrainingBatch()
{
let batchedSize = g_NumberOfBatches * getBatchTrainingSize();
let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType);
// If training limits don't allow us to train batchedSize in each appropriate structure.
if (g_BatchTrainingEntityAllowedCount !== undefined &&
g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length)
{
// Train as many full batches as we can.
let buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / batchedSize);
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch),
"template": g_BatchTrainingType,
- "count": batchedSize
+ "count": batchedSize,
+ "pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
// Train remainer in one more structure.
let remainer = g_BatchTrainingEntityAllowedCount % batchedSize;
if (remainer)
Engine.PostNetworkCommand({
"type": "train",
"entities": [appropriateBuildings[buildingsCountToTrainFullBatch]],
"template": g_BatchTrainingType,
- "count": remainer
+ "count": remainer,
+ "pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
else
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings,
"template": g_BatchTrainingType,
- "count": batchedSize
+ "count": batchedSize,
+ "pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
});
}
function performGroup(action, groupId)
{
switch (action)
{
case "snap":
case "select":
case "add":
let toSelect = [];
g_Groups.update();
for (let ent in g_Groups.groups[groupId].ents)
toSelect.push(+ent);
if (action != "add")
g_Selection.reset();
g_Selection.addList(toSelect);
if (action == "snap" && toSelect.length)
{
let entState = GetEntityState(getEntityOrHolder(toSelect[0]));
let position = entState.position;
if (position && entState.visibility != "hidden")
Engine.CameraMoveTo(position.x, position.z);
}
break;
case "save":
case "breakUp":
g_Groups.groups[groupId].reset();
if (action == "save")
g_Groups.addEntities(groupId, g_Selection.toList());
updateGroups();
break;
}
}
var lastIdleUnit = 0;
var currIdleClassIndex = 0;
var lastIdleClasses = [];
function resetIdleUnit()
{
lastIdleUnit = 0;
currIdleClassIndex = 0;
lastIdleClasses = [];
}
function findIdleUnit(classes)
{
let append = Engine.HotkeyIsPressed("selection.add");
let selectall = Engine.HotkeyIsPressed("selection.offscreen");
// Reset the last idle unit, etc., if the selection type has changed.
if (selectall || classes.length != lastIdleClasses.length || !classes.every((v, i) => v === lastIdleClasses[i]))
resetIdleUnit();
lastIdleClasses = classes;
let data = {
"viewedPlayer": g_ViewedPlayer,
"excludeUnits": append ? g_Selection.toList() : [],
// If the current idle class index is not 0, put the class at that index first.
"idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex))
};
if (!selectall)
{
data.limit = 1;
data.prevUnit = lastIdleUnit;
}
let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
if (!idleUnits.length)
{
// TODO: display a message to indicate no more idle units, or something
Engine.GuiInterfaceCall("PlaySoundForPlayer", {
"name": "no_idle_unit"
});
resetIdleUnit();
return;
}
if (!append)
g_Selection.reset();
g_Selection.addList(idleUnits);
if (selectall)
return;
lastIdleUnit = idleUnits[0];
let entityState = GetEntityState(lastIdleUnit);
if (entityState.position)
Engine.CameraMoveTo(entityState.position.x, entityState.position.z);
// Move the idle class index to the first class an idle unit was found for.
let indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem));
currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length;
}
function clearSelection()
{
if (inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
{
inputState = INPUT_NORMAL;
placementSupport.Reset();
}
else
g_Selection.reset();
preSelectedAction = ACTION_NONE;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 25958)
@@ -1,1270 +1,1282 @@
/**
* Contains the layout and button settings per selection panel
*
* getItems returns a list of basic items used to fill the panel.
* This method is obligated. If the items list is empty, the panel
* won't be rendered.
*
* Then there's a loop over all items provided. In the loop,
* the item and some other standard data is added to a data object.
*
* The standard data is
* {
* "i": index
* "item": item coming from the getItems function
* "playerState": playerState
* "unitEntStates": states of the selected entities
* "rowLength": rowLength
* "numberOfItems": number of items that will be processed
* "button": gui Button object
* "icon": gui Icon object
* "guiSelection": gui button Selection overlay
* "countDisplay": gui caption space
* }
*
* Then for every data object, the setupButton function is called which
* sets the view and handlers of the button.
*/
// Cache some formation info
// Available formations per player
var g_AvailableFormations = new Map();
var g_FormationsInfo = new Map();
var g_SelectionPanels = {};
var g_SelectionPanelBarterButtonManager;
g_SelectionPanels.Alert = {
"getMaxNumberOfItems": function()
{
return 2;
},
"getItems": function(unitEntStates)
{
return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : [];
},
"setupButton": function(data)
{
data.button.onPress = function() {
switch (data.item)
{
case "raise":
raiseAlert();
return;
case "end":
endOfAlert();
return;
}
};
switch (data.item)
{
case "raise":
data.icon.sprite = "stretched:session/icons/bell_level1.png";
data.button.tooltip = translate("Raise an alert!");
break;
case "end":
data.button.tooltip = translate("End of alert.");
data.icon.sprite = "stretched:session/icons/bell_level0.png";
break;
}
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Barter = {
"getMaxNumberOfItems": function()
{
return 5;
},
"rowLength": 5,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
// If more than `rowLength` resources, don't display icons.
if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetBarterableCodes().length > this.rowLength)
return [];
return g_ResourceData.GetBarterableCodes();
},
"setupButton": function(data)
{
if (g_SelectionPanelBarterButtonManager)
{
g_SelectionPanelBarterButtonManager.setViewedPlayer(data.player);
g_SelectionPanelBarterButtonManager.update();
}
return true;
}
};
g_SelectionPanels.Command = {
"getMaxNumberOfItems": function()
{
return 6;
},
"getItems": function(unitEntStates)
{
let commands = [];
for (let command in g_EntityCommands)
{
let info = getCommandInfo(command, unitEntStates);
if (info)
{
info.name = command;
commands.push(info);
}
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performCommand(data.unitEntStates, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled = data.item.enabled == true;
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
let size = data.button.size;
// relative to the center ( = 50%)
size.rleft = 50;
size.rright = 50;
// offset from the center calculation, count on square buttons, so size.bottom is the width too
size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1);
size.right = size.left + size.bottom;
data.button.size = size;
return true;
}
};
g_SelectionPanels.Construction = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function()
{
return getAllBuildableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item, data.player);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.player
});
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, 1),
"player": data.player
});
data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); };
let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
let tooltips = [
getEntityNamesFormatted,
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip
].map(func => func(template));
tooltips.push(
getEntityCostTooltip(template, data.player),
getResourceDropsiteTooltip(template),
getGarrisonTooltip(template),
getTurretsTooltip(template),
getPopulationBonusTooltip(template),
showTemplateViewerOnRightClickTooltip(template)
);
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
data.button.enabled = controlsPlayer(data.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Formation = {
"getMaxNumberOfItems": function()
{
return 15;
},
"rowLength": 5,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !hasClass(state, "Unit")))
return [];
if (unitEntStates.every(state => !state.identity || !state.identity.hasSomeFormation))
return [];
if (!g_AvailableFormations.has(unitEntStates[0].player))
g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player));
return g_AvailableFormations.get(unitEntStates[0].player).filter(formation => unitEntStates.some(state => !!state.identity && state.identity.formations.indexOf(formation) != -1));
},
"setupButton": function(data)
{
if (!g_FormationsInfo.has(data.item))
g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
let formationOk = canMoveSelectionIntoFormation(data.item);
let unitIds = data.unitEntStates.map(state => state.id);
let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": unitIds,
"formationTemplate": data.item
});
data.button.onPress = function() {
performFormation(unitIds, data.item);
};
data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item);
let formationInfo = g_FormationsInfo.get(data.item);
let tooltip = translate(formationInfo.name);
let isDefaultFormation = g_AutoFormation.isDefault(data.item);
if (data.item === NULL_FORMATION)
tooltip += "\n" + (isDefaultFormation ?
translate("Default formation is disabled.") :
translate("Right-click to disable the default formation feature."));
else
tooltip += "\n" + (isDefaultFormation ?
translate("This is the default formation, used for movement orders.") :
translate("Right-click to set this as the default formation."));
if (!formationOk && formationInfo.tooltip)
tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red");
data.button.tooltip = tooltip;
data.button.enabled = formationOk && controlsPlayer(data.player);
let grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.countDisplay.hidden = !isDefaultFormation;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Garrison = {
"getMaxNumberOfItems": function()
{
return 12;
},
"rowLength": 4,
"conflictsWith": ["Barter"],
"getItems": function(unitEntStates)
{
if (unitEntStates.every(state => !state.garrisonHolder))
return [];
let groups = new EntityGroups();
for (let state of unitEntStates)
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities);
return groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
data.button.onPress = function() {
unloadTemplate(template.selectionGroupName || entState.template, entState.player);
};
data.countDisplay.caption = data.item.ents.length || "";
let canUngarrison = controlsPlayer(data.player) || controlsPlayer(entState.player);
data.button.enabled = canUngarrison;
data.button.tooltip = (canUngarrison ?
sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) }) + "\n" +
translate("Single-click to unload 1. Shift-click to unload all of this type.") :
getEntityNames(template)) + "\n" +
sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[entState.player].name
});
data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(entState.player, 160);
data.button.sprite_disabled = data.button.sprite;
// Selection panel buttons only appear disabled if they
// also appear disabled to the owner of the structure.
data.icon.sprite =
(canUngarrison || g_IsObserver ? "" : "grayscale:") +
"stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Gate = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
let hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked);
let hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked);
if (hideLocked && hideUnlocked)
return [];
return [
{
"hidden": hideLocked,
"tooltip": translate("Lock Gate"),
"icon": "session/icons/lock_locked.png",
"locked": true
},
{
"hidden": hideUnlocked,
"tooltip": translate("Unlock Gate"),
"icon": "session/icons/lock_unlocked.png",
"locked": false
}
];
},
"setupButton": function(data)
{
data.button.onPress = function() { lockGate(data.item.locked); };
data.button.tooltip = data.item.tooltip;
data.button.enabled = controlsPlayer(data.player);
data.guiSelection.hidden = data.item.hidden;
data.icon.sprite = "stretched:" + data.item.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Pack = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
let checks = {};
for (let state of unitEntStates)
{
if (!state.pack)
continue;
if (state.pack.progress == 0)
{
if (state.pack.packed)
checks.unpackButton = true;
else
checks.packButton = true;
}
else if (state.pack.packed)
checks.unpackCancelButton = true;
else
checks.packCancelButton = true;
}
let items = [];
if (checks.packButton)
items.push({
"packing": false,
"packed": false,
"tooltip": translate("Pack"),
"callback": function() { packUnit(true); }
});
if (checks.unpackButton)
items.push({
"packing": false,
"packed": true,
"tooltip": translate("Unpack"),
"callback": function() { packUnit(false); }
});
if (checks.packCancelButton)
items.push({
"packing": true,
"packed": false,
"tooltip": translate("Cancel Packing"),
"callback": function() { cancelPackUnit(true); }
});
if (checks.unpackCancelButton)
items.push({
"packing": true,
"packed": true,
"tooltip": translate("Cancel Unpacking"),
"callback": function() { cancelPackUnit(false); }
});
return items;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
data.button.tooltip = data.item.tooltip;
if (data.item.packing)
data.icon.sprite = "stretched:session/icons/cancel.png";
else if (data.item.packed)
data.icon.sprite = "stretched:session/icons/unpack.png";
else
data.icon.sprite = "stretched:session/icons/pack.png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Queue = {
"getMaxNumberOfItems": function()
{
return 16;
},
/**
* Returns a list of all items in the productionqueue of the selection
* The first entry of every entity's production queue will come before
* the second entry of every entity's production queue
*/
"getItems": function(unitEntStates)
{
const queue = [];
let foundNew = true;
for (let i = 0; foundNew; ++i)
{
foundNew = false;
for (const state of unitEntStates)
{
if (!state.production || !state.production.queue[i])
continue;
queue.push({
"producingEnt": state.id,
"queuedItem": state.production.queue[i],
"autoqueue": state.production.autoqueue && state.production.queue[i].unitTemplate,
});
foundNew = true;
}
}
if (!queue.length)
return queue;
// Add 'ghost' items to show autoqueues.
const repeat = [];
for (const item of queue)
if (item.autoqueue)
{
const ghostItem = clone(item);
ghostItem.ghost = true;
repeat.push(ghostItem);
}
if (repeat.length)
for (let i = 0; queue.length < g_SelectionPanels.Queue.getMaxNumberOfItems(); ++i)
queue.push(repeat[i % repeat.length]);
return queue;
},
"resizePanel": function(numberOfItems, rowLength)
{
let numRows = Math.ceil(numberOfItems / rowLength);
let panel = Engine.GetGUIObjectByName("unitQueuePanel");
let size = panel.size;
let buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
let margin = 4;
size.top = size.bottom - numRows * buttonSize - (numRows + 2) * margin;
panel.size = size;
},
"setupButton": function(data)
{
let queuedItem = data.item.queuedItem;
// Differentiate between units and techs
let template;
if (queuedItem.unitTemplate)
template = GetTemplateData(queuedItem.unitTemplate);
else if (queuedItem.technologyTemplate)
template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ);
else
{
warning("Unknown production queue template " + uneval(queuedItem));
return false;
}
data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); };
const tooltips = [getEntityNames(template)];
if (data.item.ghost)
tooltips.push(translate("The auto-queue will try to train this item later."));
if (queuedItem.neededSlots)
{
tooltips.push(coloredText(translate("Insufficient population capacity:"), "red"));
tooltips.push(sprintf(translate("%(population)s %(neededSlots)s"), {
"population": resourceIcon("population"),
"neededSlots": queuedItem.neededSlots
}));
}
tooltips.push(showTemplateViewerOnRightClickTooltip(template));
data.button.tooltip = tooltips.join("\n");
data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";
if (data.item.ghost)
{
data.button.enabled = false;
Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]").sprite="color:0 150 250 50";
}
else
{
// Show the time remaining to finish the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueTimeRemaining").caption =
Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));
const guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
guiObject.sprite = "queueProgressSlider";
const size = guiObject.size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left));
guiObject.size = size;
data.button.enabled = controlsPlayer(data.player);
+
+ Engine.GetGUIObjectByName("unitQueuePausedIcon[" + data.i + "]").hidden = !queuedItem.paused;
+ if (queuedItem.paused)
+ // Translation: String displayed when the research is paused. E.g. by being garrisoned or when not the first item in the queue.
+ data.button.tooltip += "\n" + translate("(This item is paused.)");
}
if (template.icon)
- data.icon.sprite = (data.item.ghost ? "grayscale:" : "") + "stretched:session/portraits/" + template.icon;
+ {
+ let modifier = "stretched:";
+ if (queuedItem.paused)
+ modifier += "color:0 0 0 127:grayscale:";
+ else if (data.item.ghost)
+ modifier += "grayscale:";
+ data.icon.sprite = modifier + "session/portraits/" + template.icon;
+ }
const showTemplateFunc = () => { showTemplateDetails(data.item.queuedItem.unitTemplate || data.item.queuedItem.technologyTemplate, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Research = {
"getMaxNumberOfItems": function()
{
return 10;
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
let ret = [];
if (unitEntStates.length == 1)
return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret :
unitEntStates[0].production.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": unitEntStates[0].production.techCostMultiplier,
"researchFacilityId": unitEntStates[0].id,
"isUpgrading": !!unitEntStates[0].upgrade && unitEntStates[0].upgrade.isUpgrading
}));
let sortedEntStates = unitEntStates.sort((a, b) =>
(!b.upgrade || !b.upgrade.isUpgrading) - (!a.upgrade || !a.upgrade.isUpgrading) ||
(!a.production ? 0 : a.production.queue.length) - (!b.production ? 0 : b.production.queue.length)
);
for (let state of sortedEntStates)
{
if (!state.production || !state.production.technologies)
continue;
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
let filteredTechs = state.production.technologies.filter(
tech => tech != null && !ret.some(
item =>
(item.tech == tech ||
item.tech.pair &&
tech.pair &&
item.tech.bottom == tech.bottom &&
item.tech.top == tech.top) &&
Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k])
));
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
ret = ret.concat(filteredTechs.map(tech => ({
"tech": tech,
"techCostMultiplier": state.production.techCostMultiplier,
"researchFacilityId": state.id,
"isUpgrading": !!state.upgrade && state.upgrade.isUpgrading
})));
}
return ret;
},
"hideItem": function(i, rowLength) // Called when no item is found
{
Engine.GetGUIObjectByName("unitResearchButton[" + i + "]").hidden = true;
// We also remove the paired tech and the pair symbol
Engine.GetGUIObjectByName("unitResearchButton[" + (i + rowLength) + "]").hidden = true;
Engine.GetGUIObjectByName("unitResearchPair[" + i + "]").hidden = true;
},
"setupButton": function(data)
{
if (!data.item.tech)
{
g_SelectionPanels.Research.hideItem(data.i, data.rowLength);
return false;
}
// Start position (start at the bottom)
let position = data.i + data.rowLength;
// Only show the top button for pairs
if (!data.item.tech.pair)
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
// Set up the tech connector
let pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]");
pair.hidden = data.item.tech.pair == null;
setPanelObjectPosition(pair, data.i, data.rowLength);
// Handle one or two techs (tech pair)
let player = data.player;
let playerState = GetSimState().players[player];
for (let tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech])
{
// Don't change the object returned by GetTechnologyData
let template = clone(GetTechnologyData(tech, playerState.civ));
if (!template)
return false;
// Not allowed by civ.
if (!template.reqs)
{
// One of the pair may still be researchable by the current civ,
// hence don't hide everything.
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
pair.hidden = true;
continue;
}
for (let res in template.cost)
template.cost[res] *= data.item.techCostMultiplier[res];
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": template.cost,
"player": player
});
let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
"tech": tech,
"player": player
});
let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]");
let tooltips = [
getEntityNamesFormatted,
getEntityTooltip,
getEntityCostTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template));
if (!requirementsPassed)
{
let tip = template.requirementsTooltip;
let reqs = template.reqs;
for (let req of reqs)
{
if (!req.entities)
continue;
let entityCounts = [];
for (let entity of req.entities)
{
let current = 0;
switch (entity.check)
{
case "count":
current = playerState.classCounts[entity.class] || 0;
break;
case "variants":
current = playerState.typeCountsByClass[entity.class] ?
Object.keys(playerState.typeCountsByClass[entity.class]).length : 0;
break;
}
let remaining = entity.number - current;
if (remaining < 1)
continue;
entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), {
"number": remaining,
"class": translate(entity.class)
}));
}
tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), {
"entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", "))
});
}
tooltips.push(tip);
}
tooltips.push(getNeededResourcesTooltip(neededResources));
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (t => function() {
addResearchToQueue(data.item.researchFacilityId, t);
})(tech);
let showTemplateFunc = (t => function() {
showTemplateDetails(
t,
GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv);
});
button.onPressRight = showTemplateFunc(tech);
button.onPressRightDisabled = showTemplateFunc(tech);
if (data.item.tech.pair)
{
// On mouse enter, show a cross over the other icon
let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]");
button.onMouseEnter = function() {
unchosenIcon.hidden = false;
};
button.onMouseLeave = function() {
unchosenIcon.hidden = true;
};
}
button.hidden = false;
let modifier = "";
if (!requirementsPassed)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
button.enabled = controlsPlayer(data.player);
if (data.item.isUpgrading)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
button.tooltip += "\n" + coloredText(translate("Cannot research while upgrading."), "red");
}
if (template.icon)
icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(button, position, data.rowLength);
// Prepare to handle the top button (if any)
position -= data.rowLength;
}
return true;
}
};
g_SelectionPanels.Selection = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"getItems": function(unitEntStates)
{
if (unitEntStates.length < 2)
return [];
return g_Selection.groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
for (let ent of data.item.ents)
{
let state = GetEntityState(ent);
if (state.resourceCarrying && state.resourceCarrying.length !== 0)
{
if (!data.carried)
data.carried = {};
let carrying = state.resourceCarrying[0];
if (data.carried[carrying.type])
data.carried[carrying.type] += carrying.amount;
else
data.carried[carrying.type] = carrying.amount;
}
if (state.trader && state.trader.goods && state.trader.goods.amount)
{
if (!data.carried)
data.carried = {};
let amount = state.trader.goods.amount;
let type = state.trader.goods.type;
let totalGain = amount.traderGain;
if (amount.market1Gain)
totalGain += amount.market1Gain;
if (amount.market2Gain)
totalGain += amount.market2Gain;
if (data.carried[type])
data.carried[type] += totalGain;
else
data.carried[type] = totalGain;
}
}
let unitOwner = GetEntityState(data.item.ents[0]).player;
let tooltip = getEntityNames(template);
if (data.carried)
tooltip += "\n" + Object.keys(data.carried).map(res =>
resourceIcon(res) + data.carried[res]
).join(" ");
if (g_IsObserver)
tooltip += "\n" + sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[unitOwner].name
});
data.button.tooltip = tooltip;
data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(unitOwner, 160);
data.guiSelection.hidden = !g_IsObserver;
data.countDisplay.caption = data.item.ents.length || "";
data.button.onPress = function() {
if (Engine.HotkeyIsPressed("session.deselectgroup"))
removeFromSelectionGroup(data.item.key);
else
makePrimarySelectionGroup(data.item.key);
};
data.button.onPressRight = function() { removeFromSelectionGroup(data.item.key); };
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Stance = {
"getMaxNumberOfItems": function()
{
return 5;
},
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")))
return [];
return unitEntStates[0].unitAI.selectableStances;
},
"setupButton": function(data)
{
let unitIds = data.unitEntStates.map(state => state.id);
data.button.onPress = function() { performStance(unitIds, data.item); };
data.button.tooltip = getStanceDisplayName(data.item) + "\n" +
"[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]";
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": unitIds,
"stance": data.item
});
data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Training = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function()
{
return getAllTrainableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item, data.player);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.player
});
let unitIds = data.unitEntStates.map(status => status.id);
let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingStatus(unitIds, data.item, data.playerState);
let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, trainNum),
"player": data.player
});
data.button.onPress = function() {
if (!neededResources)
addTrainingToQueue(unitIds, data.item, data.playerState);
};
let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
data.countDisplay.caption = trainNum > 1 ? trainNum : "";
let tooltips = [
"[font=\"sans-bold-16\"]" +
colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
"[/font]" + " " + getEntityNamesFormatted(template),
getVisibleEntityClassesFormatted(template),
getAurasTooltip(template),
getEntityTooltip(template),
getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
];
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type));
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
tooltips = tooltips.concat([
getHealthTooltip,
getAttackTooltip,
getHealerTooltip,
getResistanceTooltip,
getGarrisonTooltip,
getTurretsTooltip,
getProjectilesTooltip,
getSpeedTooltip,
getResourceDropsiteTooltip
].map(func => func(template)));
tooltips.push(showTemplateViewerOnRightClickTooltip());
tooltips.push(
formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch),
getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else
{
data.button.enabled = controlsPlayer(data.player);
if (neededResources)
modifier = resourcesToAlphaMask(neededResources) + ":";
}
if (data.unitEntStates.every(state => state.upgrade && state.upgrade.isUpgrading))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
data.button.tooltip += "\n" + coloredText(translate("Cannot train while upgrading."), "red");
}
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Upgrade = {
"getMaxNumberOfItems": function()
{
return 40 - getNumberOfRightPanelButtons();
},
"rowLength": 10,
"getItems": function(unitEntStates)
{
// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
if (unitEntStates.some(state => state.template != unitEntStates[0].template))
return false;
return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades;
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item.entity);
if (!template)
return false;
let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]");
progressOverlay.hidden = true;
let technologyEnabled = true;
if (data.item.requiredTechnology)
technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": data.item.requiredTechnology,
"player": data.player
});
let limits = getEntityLimitAndCount(data.playerState, data.item.entity);
let upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.template == data.item.entity);
let upgradableEntStates = data.unitEntStates.filter(state =>
!state.upgrade.progress &&
(!state.production || !state.production.queue || !state.production.queue.length));
let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(data.item, upgradableEntStates.length),
"player": data.player
});
let tooltip;
let modifier = "";
if (!upgradingEntStates.length && upgradableEntStates.length)
{
let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
let secondaryName;
if (g_ShowSecondaryNames)
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
let tooltips = [];
if (g_ShowSecondaryNames)
{
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s). %(tooltip)s"), {
"primaryName": primaryName,
"secondaryName": secondaryName,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s)."), {
"primaryName": primaryName,
"secondaryName": secondaryName
}));
}
else
{
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s. %(tooltip)s"), {
"primaryName": primaryName,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s."), {
"primaryName": primaryName
}));
}
tooltips.push(
getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length),
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources),
showTemplateViewerOnRightClickTooltip());
tooltip = tooltips.filter(tip => tip).join("\n");
data.button.onPress = function() {
upgradeEntity(
data.item.entity,
upgradableEntStates.map(state => state.id));
};
if (!technologyEnabled || limits.canBeAddedCount == 0 &&
!upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.template)))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier = resourcesToAlphaMask(neededResources) + ":";
}
data.countDisplay.caption = upgradableEntStates.length > 1 ? upgradableEntStates.length : "";
}
else if (upgradingEntStates.length)
{
tooltip = translate("Cancel Upgrading");
data.button.onPress = function() { cancelUpgradeEntity(); };
data.countDisplay.caption = upgradingEntStates.length > 1 ? upgradingEntStates.length : "";
let progress = 0;
for (let state of upgradingEntStates)
progress = Math.max(progress, state.upgrade.progress || 1);
let progressOverlaySize = progressOverlay.size;
// TODO This is bad: we assume the progressOverlay is square
progressOverlaySize.top = progressOverlaySize.bottom + Math.round((1 - progress) * (progressOverlaySize.left - progressOverlaySize.right));
progressOverlay.size = progressOverlaySize;
progressOverlay.hidden = false;
}
else
{
tooltip = coloredText(translatePlural(
"Cannot upgrade when the entity is training, researching or already upgrading.",
"Cannot upgrade when all entities are training, researching or already upgrading.",
data.unitEntStates.length), "red");
data.button.onPress = function() {};
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
data.button.enabled = controlsPlayer(data.player);
data.button.tooltip = tooltip;
let showTemplateFunc = () => { showTemplateDetails(data.item.entity, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;
data.button.onPressRightDisabled = showTemplateFunc;
data.icon.sprite = modifier + "stretched:session/" +
(data.item.icon || "portraits/" + template.icon);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
function initSelectionPanels()
{
let unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel");
if (BarterButtonManager.IsAvailable(unitBarterPanel))
g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel);
}
/**
* Pauses game and opens the template details viewer for a selected entity or technology.
*
* Technologies don't have a set civ, so we pass along the native civ of
* the template of the entity that's researching it.
*
* @param {string} [civCode] - The template name of the entity that researches the selected technology.
*/
function showTemplateDetails(templateName, civCode)
{
if (inputState != INPUT_NORMAL)
return;
g_PauseControl.implicitPause();
Engine.PushGuiPage(
"page_viewer.xml",
{
"templateName": templateName,
"civ": civCode
},
resumeGame);
}
/**
* If two panels need the same space, so they collide,
* the one appearing first in the order is rendered.
*
* Note that the panel needs to appear in the list to get rendered.
*/
let g_PanelsOrder = [
// LEFT PANE
"Barter", // Must always be visible on markets
"Garrison", // More important than Formation, as you want to see the garrisoned units in ships
"Alert",
"Formation",
"Stance", // Normal together with formation
// RIGHT PANE
"Gate", // Must always be shown on gates
"Pack", // Must always be shown on packable entities
"Upgrade", // Must always be shown on upgradable entities
"Training",
"Construction",
"Research", // Normal together with training
// UNIQUE PANES (importance doesn't matter)
"Command",
"Queue",
"Selection",
];
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25958)
@@ -1,576 +1,577 @@
/**
* @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)
{
let template1 = GetTemplateData(templateName1);
let template2 = GetTemplateData(templateName2);
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 (let 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 = coloredText(text, "red");
for (var c in trainEntLimitChangers)
{
if (!trainEntLimitChangers[c])
continue;
let 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 "";
let passedLimit = matchEntCount >= matchEntLimit;
let count = matchEntLimit - matchEntCount;
let text;
if (type == "build")
{
if (passedLimit)
text = sprintf(translatePlural("Could only be constructed once.", "Could only be constructed %(limit)s times.", matchEntLimit), {
"limit": matchEntLimit
});
else if (matchEntLimit == 1)
text = translate("Can be constructed only once.");
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 ? coloredText(text, "red") : 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)
{
let position = g_JumpCameraPositions[index];
if (!position)
return;
let threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
let 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
+ "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
});
}
/**
* Set the camera to follow the given entity if it's a unit.
* Otherwise stop following.
*/
function setCameraFollow(entity)
{
let entState = entity && GetEntityState(entity);
if (entState && hasClass(entState, "Unit"))
Engine.CameraFollow(entity);
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 => {
let state = GetEntityState(ent);
return state && !!state.garrisonHolder;
})
});
}
function unloadAll()
{
const garrisonHolders = g_Selection.filter(e => {
let state = GetEntityState(e);
return state && !!state.garrisonHolder;
});
if (!garrisonHolders.length)
return;
let ownEnts = [];
let otherEnts = [];
for (let 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 => {
let state = GetEntityState(e);
return state && !!state.turretHolder;
});
if (!turretHolders.length)
return;
let ownedHolders = [];
let ejectables = [];
for (let ent of turretHolders)
{
let turretHolderState = GetEntityState(ent);
if (controlsPlayer(turretHolderState.player))
ownedHolders.push(ent);
else
{
for (let 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 => {
let 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 => {
let 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 => {
let state = GetEntityState(ent);
return state && state.unitAI && state.unitAI.isGuarding;
})
});
}
function raiseAlert()
{
Engine.PostNetworkCommand({
"type": "alert-raise",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
function endOfAlert()
{
Engine.PostNetworkCommand({
"type": "alert-end",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return state && !!state.alertRaiser;
})
});
}
function turnAutoQueueOn()
{
Engine.PostNetworkCommand({
"type": "autoqueue-on",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return !!state?.production?.entities.length &&
!state.production.autoqueue;
})
});
}
function turnAutoQueueOff()
{
Engine.PostNetworkCommand({
"type": "autoqueue-off",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return !!state?.production?.entities.length &&
state.production.autoqueue;
})
});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_right/queue_panel.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_right/queue_panel.xml (revision 25957)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_right/queue_panel.xml (revision 25958)
@@ -1,20 +1,21 @@
Production queue
-
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 25958)
@@ -1,1021 +1,1027 @@
var API3 = function(m)
{
// defines a template.
m.Template = m.Class({
"_init": function(sharedAI, templateName, template)
{
this._templateName = templateName;
this._template = template;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
this._tpCache = new Map();
},
// Helper function to return a template value, adjusting for tech.
"get": function(string)
{
if (this._entityModif && this._entityModif.has(string))
return this._entityModif.get(string);
else if (this._templateModif)
{
let owner = this._entity ? this._entity.owner : PlayerID;
if (this._templateModif[owner] && this._templateModif[owner].has(string))
return this._templateModif[owner].get(string);
}
if (!this._tpCache.has(string))
{
let value = this._template;
let args = string.split("/");
for (let arg of args)
{
value = value[arg];
if (value == undefined)
break;
}
this._tpCache.set(string, value);
}
return this._tpCache.get(string);
},
"templateName": function() { return this._templateName; },
"genericName": function() { return this.get("Identity/GenericName"); },
"civ": function() { return this.get("Identity/Civ"); },
"matchLimit": function() {
if (!this.get("TrainingRestrictions"))
return undefined;
return this.get("TrainingRestrictions/MatchLimit");
},
"classes": function() {
let template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
},
"hasClass": function(name) {
if (!this._classes)
this._classes = this.classes();
return this._classes && this._classes.indexOf(name) != -1;
},
"hasClasses": function(array) {
if (!this._classes)
this._classes = this.classes();
return this._classes && MatchesClassList(this._classes, array);
},
"requiredTech": function() { return this.get("Identity/RequiredTechnology"); },
"available": function(gameState) {
let techRequired = this.requiredTech();
if (!techRequired)
return true;
return gameState.isResearched(techRequired);
},
// specifically
"phase": function() {
let techRequired = this.requiredTech();
if (!techRequired)
return 0;
if (techRequired == "phase_village")
return 1;
if (techRequired == "phase_town")
return 2;
if (techRequired == "phase_city")
return 3;
if (techRequired.startsWith("phase_"))
return 4;
return 0;
},
"cost": function(productionQueue) {
if (!this.get("Cost"))
return {};
let ret = {};
for (let type in this.get("Cost/Resources"))
ret[type] = +this.get("Cost/Resources/" + type);
return ret;
},
"costSum": function(productionQueue) {
let cost = this.cost(productionQueue);
if (!cost)
return 0;
let ret = 0;
for (let type in cost)
ret += cost[type];
return ret;
},
"techCostMultiplier": function(type) {
return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1);
},
/**
* Returns { "max": max, "min": min } or undefined if no obstruction.
* max: radius of the outer circle surrounding this entity's obstruction shape
* min: radius of the inner circle
*/
"obstructionRadius": function() {
if (!this.get("Obstruction"))
return undefined;
if (this.get("Obstruction/Static"))
{
let w = +this.get("Obstruction/Static/@width");
let h = +this.get("Obstruction/Static/@depth");
return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
}
if (this.get("Obstruction/Unit"))
{
let r = +this.get("Obstruction/Unit/@radius");
return { "max": r, "min": r };
}
let right = this.get("Obstruction/Obstructions/Right");
let left = this.get("Obstruction/Obstructions/Left");
if (left && right)
{
let w = +right["@x"] + right["@width"] / 2 - left["@x"] + left["@width"] / 2;
let h = Math.max(+right["@z"] + right["@depth"] / 2, +left["@z"] + left["@depth"] / 2) -
Math.min(+right["@z"] - right["@depth"] / 2, +left["@z"] - left["@depth"] / 2);
return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
}
return { "max": 0, "min": 0 }; // Units have currently no obstructions
},
/**
* Returns the radius of a circle surrounding this entity's footprint.
*/
"footprintRadius": function() {
if (!this.get("Footprint"))
return undefined;
if (this.get("Footprint/Square"))
{
let w = +this.get("Footprint/Square/@width");
let h = +this.get("Footprint/Square/@depth");
return Math.sqrt(w * w + h * h) / 2;
}
if (this.get("Footprint/Circle"))
return +this.get("Footprint/Circle/@radius");
return 0; // this should never happen
},
"maxHitpoints": function() { return +(this.get("Health/Max") || 0); },
"isHealable": function()
{
if (this.get("Health") !== undefined)
return this.get("Health/Unhealable") !== "true";
return false;
},
"isRepairable": function() { return this.get("Repairable") !== undefined; },
"getPopulationBonus": function() {
if (!this.get("Population"))
return 0;
return +this.get("Population/Bonus");
},
"resistanceStrengths": function() {
let resistanceTypes = this.get("Resistance");
if (!resistanceTypes || !resistanceTypes.Entity)
return undefined;
let resistance = {};
if (resistanceTypes.Entity.Capture)
resistance.Capture = +this.get("Resistance/Entity/Capture");
if (resistanceTypes.Entity.Damage)
{
resistance.Damage = {};
for (let damageType in resistanceTypes.Entity.Damage)
resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType);
}
// ToDo: Resistance to StatusEffects.
return resistance;
},
"attackTypes": function() {
let attack = this.get("Attack");
if (!attack)
return undefined;
let ret = [];
for (let type in attack)
ret.push(type);
return ret;
},
"attackRange": function(type) {
if (!this.get("Attack/" + type))
return undefined;
return {
"max": +this.get("Attack/" + type +"/MaxRange"),
"min": +(this.get("Attack/" + type +"/MinRange") || 0)
};
},
"attackStrengths": function(type) {
let attackDamageTypes = this.get("Attack/" + type + "/Damage");
if (!attackDamageTypes)
return undefined;
let damage = {};
for (let damageType in attackDamageTypes)
damage[damageType] = +attackDamageTypes[damageType];
return damage;
},
"captureStrength": function() {
if (!this.get("Attack/Capture"))
return undefined;
return +this.get("Attack/Capture/Capture") || 0;
},
"attackTimes": function(type) {
if (!this.get("Attack/" + type))
return undefined;
return {
"prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0),
"repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000)
};
},
// returns the classes this templates counters:
// Return type is [ [-neededClasses- , multiplier], … ].
"getCounteredClasses": function() {
let attack = this.get("Attack");
if (!attack)
return undefined;
let Classes = [];
for (let type in attack)
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]);
}
}
return Classes;
},
// returns true if the entity counters the target entity.
// TODO: refine using the multiplier
"counters": function(target) {
let attack = this.get("Attack");
if (!attack)
return false;
let mcounter = [];
for (let type in attack)
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
mcounter.concat(bonusClasses.split(" "));
}
}
return target.hasClasses(mcounter);
},
// returns, if it exists, the multiplier from each attack against a given class
"getMultiplierAgainst": function(type, againstClass) {
if (!this.get("Attack/" + type +""))
return undefined;
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (bonuses)
{
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (!bonusClasses)
continue;
for (let bcl of bonusClasses.split(" "))
if (bcl == againstClass)
return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
}
}
return 1;
},
"buildableEntities": function(civ) {
let templates = this.get("Builder/Entities/_string");
if (!templates)
return [];
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"trainableEntities": function(civ) {
let templates = this.get("ProductionQueue/Entities/_string");
if (!templates)
return undefined;
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"researchableTechs": function(gameState, civ) {
let templates = this.get("ProductionQueue/Technologies/_string");
if (!templates)
return undefined;
let techs = templates.split(/\s+/);
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let civTech = tech.replace("{civ}", civ);
techs[i] = TechnologyTemplates.Has(civTech) ?
civTech : tech.replace("{civ}", "generic");
}
return techs;
},
"resourceSupplyType": function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
},
"getResourceType": function() {
if (!this.get("ResourceSupply"))
return undefined;
return this.get("ResourceSupply/Type").split('.')[0];
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
"resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },
"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
"resourceGatherRates": function() {
if (!this.get("ResourceGatherer"))
return undefined;
let ret = {};
let baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
for (let r in this.get("ResourceGatherer/Rates"))
ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
return ret;
},
"resourceDropsiteTypes": function() {
if (!this.get("ResourceDropsite"))
return undefined;
let types = this.get("ResourceDropsite/Types");
return types ? types.split(/\s+/) : [];
},
"isResourceDropsite": function(resourceType) {
const types = this.resourceDropsiteTypes();
return types && (!resourceType || types.indexOf(resourceType) !== -1);
},
"isTreasure": function() { return this.get("Treasure") !== undefined; },
"treasureResources": function() {
if (!this.get("Treasure"))
return undefined;
let ret = {};
for (let r in this.get("Treasure/Resources"))
ret[r] = +this.get("Treasure/Resources/" + r);
return ret;
},
"garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },
"garrisonMax": function() { return this.get("GarrisonHolder/Max"); },
"garrisonSize": function() { return this.get("Garrisonable/Size"); },
"garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); },
"getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); },
"getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); },
"getGarrisonArrowClasses": function() {
if (!this.get("BuildingAI"))
return undefined;
return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
},
"buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); },
"promotion": function() { return this.get("Promotion/Entity"); },
"isPackable": function() { return this.get("Pack") != undefined; },
"isHuntable": function() {
// Do not hunt retaliating animals (dead animals can be used).
// Assume entities which can attack, will attack.
return this.get("ResourceSupply/KillBeforeGather") &&
(!this.get("Health") || !this.get("Attack"));
},
"walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); },
"trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },
"buildTime": function(productionQueue) {
let time = +this.get("Cost/BuildTime");
if (productionQueue)
time *= productionQueue.techCostMultiplier("time");
return time;
},
"buildCategory": function() { return this.get("BuildRestrictions/Category"); },
"buildDistance": function() {
let distance = this.get("BuildRestrictions/Distance");
if (!distance)
return undefined;
let ret = {};
for (let key in distance)
ret[key] = this.get("BuildRestrictions/Distance/" + key);
return ret;
},
"buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); },
"buildTerritories": function() {
if (!this.get("BuildRestrictions"))
return undefined;
let territory = this.get("BuildRestrictions/Territory");
return !territory ? undefined : territory.split(/\s+/);
},
"hasBuildTerritory": function(territory) {
let territories = this.buildTerritories();
return territories && territories.indexOf(territory) != -1;
},
"hasTerritoryInfluence": function() {
return this.get("TerritoryInfluence") !== undefined;
},
"hasDefensiveFire": function() {
if (!this.get("Attack/Ranged"))
return false;
return this.getDefaultArrow() || this.getArrowMultiplier();
},
"territoryInfluenceRadius": function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Radius");
return -1;
},
"territoryInfluenceWeight": function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Weight");
return -1;
},
"territoryDecayRate": function() {
return +(this.get("TerritoryDecay/DecayRate") || 0);
},
"defaultRegenRate": function() {
return +(this.get("Capturable/RegenRate") || 0);
},
"garrisonRegenRate": function() {
return +(this.get("Capturable/GarrisonRegenRate") || 0);
},
"visionRange": function() { return +this.get("Vision/Range"); },
"gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); },
"isBuilder": function() { return this.get("Builder") !== undefined; },
"isGatherer": function() { return this.get("ResourceGatherer") !== undefined; },
"canGather": function(type) {
let gatherRates = this.get("ResourceGatherer/Rates");
if (!gatherRates)
return false;
for (let r in gatherRates)
if (r.split('.')[0] === type)
return true;
return false;
},
"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; },
"isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },
/**
* returns true if the tempalte can capture the given target entity
* if no target is given, returns true if the template has the Capture attack
*/
"canCapture": function(target)
{
if (!this.get("Attack/Capture"))
return false;
if (!target)
return true;
if (!target.get("Capturable"))
return false;
let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
return !restrictedClasses || !target.hasClasses(restrictedClasses);
},
"isCapturable": function() { return this.get("Capturable") !== undefined; },
"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
"canGarrison": function() { return "Garrisonable" in this._template; },
"canOccupyTurret": function() { return "Turretable" in this._template; },
"isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; },
});
// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
m.Entity = m.Class({
"_super": m.Template,
"_init": function(sharedAI, entity)
{
this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template));
this._entity = entity;
this._ai = sharedAI;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
// save a reference to the entity tech/aura modifications
if (!sharedAI._entitiesModifications.has(entity.id))
sharedAI._entitiesModifications.set(entity.id, new Map());
this._entityModif = sharedAI._entitiesModifications.get(entity.id);
},
"toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },
"id": function() { return this._entity.id; },
/**
* Returns extra data that the AI scripts have associated with this entity,
* for arbitrary local annotations.
* (This data should not be shared with any other AI scripts.)
*/
"getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); },
/**
* Sets extra data to be associated with this entity.
*/
"setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); },
"deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; },
"deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); },
"position": function() { return this._entity.position; },
"angle": function() { return this._entity.angle; },
"isIdle": function() { return this._entity.idle; },
"getStance": function() { return this._entity.stance; },
"unitAIState": function() { return this._entity.unitAIState; },
"unitAIOrderData": function() { return this._entity.unitAIOrderData; },
"hitpoints": function() { return this._entity.hitpoints; },
"isHurt": function() { return this.hitpoints() < this.maxHitpoints(); },
"healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); },
"needsHeal": function() { return this.isHurt() && this.isHealable(); },
"needsRepair": function() { return this.isHurt() && this.isRepairable(); },
"decaying": function() { return this._entity.decaying; },
"capturePoints": function() {return this._entity.capturePoints; },
"isInvulnerable": function() { return this._entity.invulnerability || false; },
"isSharedDropsite": function() { return this._entity.sharedDropsite === true; },
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
"trainingQueue": function() {
return this._entity.trainingQueue;
},
"trainingQueueTime": function() {
let queue = this._entity.trainingQueue;
if (!queue)
return undefined;
let time = 0;
for (let item of queue)
time += item.timeRemaining;
return time / 1000;
},
"foundationProgress": function() {
return this._entity.foundationProgress;
},
"getBuilders": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return [];
return this._entity.foundationBuilders;
},
"getBuildersNb": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return 0;
return this._entity.foundationBuilders.length;
},
"owner": function() {
return this._entity.owner;
},
"isOwn": function(player) {
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === player;
},
"resourceSupplyAmount": function() {
return this._entity.resourceSupplyAmount;
},
"resourceSupplyNumGatherers": function()
{
return this._entity.resourceSupplyNumGatherers;
},
"isFull": function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this.maxGatherers() === this._entity.resourceSupplyNumGatherers;
return undefined;
},
"resourceCarrying": function() {
return this._entity.resourceCarrying;
},
"currentGatherRate": function() {
// returns the gather rate for the current target if applicable.
if (!this.get("ResourceGatherer"))
return undefined;
if (this.unitAIOrderData().length &&
this.unitAIState().split(".")[1] == "GATHER")
{
let res;
// this is an abuse of "_ai" but it works.
if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[0].target);
else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[1].target);
if (!res)
return 0;
let type = res.resourceSupplyType();
if (!type)
return 0;
let tstring = type.generic + "." + type.specific;
let rate = +this.get("ResourceGatherer/BaseSpeed");
rate *= +this.get("ResourceGatherer/Rates/" +tstring);
if (rate)
return rate;
return 0;
}
return undefined;
},
"garrisonHolderID": function() {
return this._entity.garrisonHolderID;
},
"garrisoned": function() { return this._entity.garrisoned; },
"garrisonedSlots": function() {
let count = 0;
if (this._entity.garrisoned)
for (let ent of this._entity.garrisoned)
count += +this._ai._entities.get(ent).garrisonSize();
return count;
},
"canGarrisonInside": function()
{
return this.garrisonedSlots() < this.garrisonMax();
},
/**
* returns true if the entity can attack (including capture) the given class.
*/
"canAttackClass": function(aClass)
{
let attack = this.get("Attack");
if (!attack)
return false;
for (let type in attack)
{
if (type == "Slaughter")
continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
return true;
}
return false;
},
/**
* Derived from Attack.js' similary named function.
* @return {boolean} - Whether an entity can attack a given target.
*/
"canAttackTarget": function(target, allowCapture)
{
let attackTypes = this.get("Attack");
if (!attackTypes)
return false;
let canCapture = allowCapture && this.canCapture(target);
let health = target.get("Health");
if (!health)
return canCapture;
for (let type in attackTypes)
{
if (type == "Capture" ? !canCapture : target.isInvulnerable())
continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !target.hasClasses(restrictedClasses))
return true;
}
return false;
},
"move": function(x, z, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront });
return this;
},
"moveToRange": function(x, z, min, max, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront });
return this;
},
"attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
// violent, aggressive, defensive, passive, standground
"setStance": function(stance) {
if (this.getStance() === undefined)
return undefined;
Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance});
return this;
},
"stopMoving": function() {
Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false });
},
"unload": function(id) {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] });
return this;
},
// Unloads all owned units, don't unload allies
"unloadAll": function() {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
return this;
},
"garrison": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"occupy-turret": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"attack": function(unitId, allowCapture = true, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
"collectTreasure": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, {
"type": "collect-treasure",
"entities": [this.id()],
"target": target.id(),
"queued": queued,
"pushFront": pushFront
});
return this;
},
// moveApart from a point in the opposite direction with a distance dist
"moveApart": function(point, dist) {
if (this.position() !== undefined)
{
let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
let norm = m.VectorDistance(point, this.position());
if (norm === 0)
direction = [1, 0];
else
{
direction[0] /= norm;
direction[1] /= norm;
}
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false, "pushFront": false });
}
return this;
},
// Flees from a unit in the opposite direction.
"flee": function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined)
{
let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
this.position()[1] - unitToFleeFrom.position()[1]];
let dist = m.VectorDistance(unitToFleeFrom.position(), this.position());
FleeDirection[0] = 40 * FleeDirection[0] / dist;
FleeDirection[1] = 40 * FleeDirection[1] / dist;
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false, "pushFront": false });
}
return this;
},
"gather": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"repair": function(target, autocontinue = false, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront });
return this;
},
"returnResources": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"destroy": function() {
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
return this;
},
"barter": function(buyType, sellType, amount) {
Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
return this;
},
"tradeRoute": function(target, source) {
Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false });
return this;
},
"setRallyPoint": function(target, command) {
let data = { "command": command, "target": target.id() };
Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
return this;
},
"unsetRallyPoint": function() {
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] });
return this;
},
- "train": function(civ, type, count, metadata, promotedTypes)
+ "train": function(civ, type, count, metadata, promotedTypes, pushFront = false)
{
let trainable = this.trainableEntities(civ);
if (!trainable)
{
error("Called train("+type+", "+count+") on non-training entity "+this);
return this;
}
if (trainable.indexOf(type) == -1)
{
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
return this;
}
Engine.PostCommand(PlayerID, {
"type": "train",
"entities": [this.id()],
"template": type,
"count": count,
"metadata": metadata,
- "promoted": promotedTypes
+ "promoted": promotedTypes,
+ "pushFront": pushFront
});
return this;
},
"construct": function(template, x, z, angle, metadata) {
// TODO: verify this unit can construct this, just for internal
// sanity-checking and error reporting
Engine.PostCommand(PlayerID, {
"type": "construct",
"entities": [this.id()],
"template": template,
"x": x,
"z": z,
"angle": angle,
"autorepair": false,
"autocontinue": false,
"queued": false,
"pushFront": false,
"metadata": metadata // can be undefined
});
return this;
},
- "research": function(template) {
- Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template });
+ "research": function(template, pushFront = false) {
+ Engine.PostCommand(PlayerID, {
+ "type": "research",
+ "entity": this.id(),
+ "template": template,
+ "pushFront": pushFront
+ });
return this;
},
"stopProduction": function(id) {
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
return this;
},
"stopAllProduction": function(percentToStopAt) {
let queue = this._entity.trainingQueue;
if (!queue)
return true; // no queue, so technically we stopped all production.
for (let item of queue)
if (item.progress < percentToStopAt)
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id });
return this;
},
"guard": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"removeGuard": function() {
Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
return this;
}
});
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25958)
@@ -1,2154 +1,2157 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"resourceGatherers": cmpPlayer.GetResourceGatherers(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": cmpPlayer.CanBarter(),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
/**
* Called when the game ends if the current game is part of a campaign run.
*/
GuiInterface.prototype.GetCampaignGameEndData = function(player)
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (Trigger.prototype.OnCampaignGameEnd)
return Trigger.prototype.OnCampaignGameEnd();
return {};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
if (!ent)
return null;
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList(),
"controllable": cmpIdentity.IsControllable()
};
const cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
ret.formation = {
"members": cmpFormation.GetMembers()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpPopulation = Engine.QueryInterface(ent, IID_Population);
if (cmpPopulation)
ret.population = {
"bonus": cmpPopulation.GetPopBonus()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo(),
"isUpgrading": cmpUpgrade.IsUpgrading()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue(),
"autoqueue": cmpProductionQueue.IsAutoQueueing()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (cmpTurretable)
ret.turretable = {
"ejectable": cmpTurretable.IsEjectable(),
"holder": cmpTurretable.HolderID()
};
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
ret.garrisonable = {
"holder": cmpGarrisonable.HolderID(),
"size": cmpGarrisonable.UnitSize()
};
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle(),
"formation": cmpUnitAI.GetFormationController()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].attackName = cmpAttack.GetAttackName(type);
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = QueryMiragedInterface(ent, IID_Resistance);
if (cmpResistance)
ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"health": cmpHeal.GetHealth(),
"range": cmpHeal.GetRange().max,
"interval": cmpHeal.GetInterval(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetInterval(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
if (cmpTreasure)
ret.treasure = {
"collectTime": cmpTreasure.CollectionTime(),
"resources": cmpTreasure.Resources()
};
let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector);
if (cmpTreasureCollector)
ret.treasureCollector = true;
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(),
"acceleration": cmpUnitMotion.GetAcceleration()
};
let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
if (cmpUpkeep)
ret.upkeep = {
"interval": cmpUpkeep.GetInterval(),
"rates": cmpUpkeep.GetRates()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
{
let auraTemplate = AuraTemplates.Get(name);
if (!auraTemplate)
error("Template " + templateName + " has undefined aura " + name);
else
aurasTemplate[name] = auraTemplate;
}
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
- ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
- ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
+ const research = cmpProductionQueue.GetQueue().find(item => item.technologyTemplate === tech);
+ ret[tech].progress = research.progress;
+ ret[tech].timeRemaining = research.timeRemaining;
+ ret[tech].paused = research.paused;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
+ ret[tech].paused = true;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
if (!cmpAttackDetection)
return [];
return cmpAttackDetection.GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
if (!cmpPlayer)
return [];
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
cmpOwnership.SetOwner(cmd.owner);
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySoundForPlayer = function(player, data)
{
let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player);
let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound);
if (!cmpSound)
return;
let soundGroup = cmpSound.GetSoundGroup(data.name);
if (soundGroup)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player);
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle())
return { "idle": false };
let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable);
if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned())
return { "idle": false };
const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable);
if (cmpTurretable && cmpTurretable.IsTurreted())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market);
return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return [];
return cmpPlayer.GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
"GetCampaignGameEndData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"PlaySoundForPlayer": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25958)
@@ -1,1001 +1,1018 @@
function ProductionQueue() {}
ProductionQueue.prototype.Schema =
"Allows the building to train new units and research technologies" +
"" +
"0.7" +
"" +
"\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
Resources.BuildSchema("nonNegativeDecimal", ["time"]) +
"";
ProductionQueue.prototype.ProgressInterval = 1000;
ProductionQueue.prototype.MaxQueueSize = 16;
ProductionQueue.prototype.Init = function()
{
this.nextID = 1;
this.queue = [];
/**
Queue items are:
{
"id": 1,
"player": 1, // Who paid for this batch; we need this to cope with refunds cleanly.
"productionStarted": false, // true iff production has started (we have reserved population).
"timeTotal": 15000, // msecs
"timeRemaining": 10000, // msecs
+ "paused": false, // Whether the item is currently paused (e.g. not the first item or parent is garrisoned).
"resources": { "wood": 100, ... }, // Total resources of the item when queued.
"entity": {
"template": "units/example",
"count": 10,
"neededSlots": 3, // Number of population slots missing for production to begin.
"population": 1, // Population per unit, multiply by count to get total.
"resources": { "wood": 100, ... }, // Resources per entity, multiply by count to get total.
"entityCache": [189, ...], // The entities created but not spawned yet.
},
"technology": {
"template": "example_tech",
"resources": { "wood": 100, ... },
}
}
*/
};
/*
* Returns list of entities that can be trained by this building.
*/
ProductionQueue.prototype.GetEntitiesList = function()
{
return Array.from(this.entitiesMap.values());
};
/**
* @return {boolean} - Whether we are automatically queuing items.
*/
ProductionQueue.prototype.IsAutoQueueing = function()
{
return !!this.autoqueuing;
};
/**
* Turn on Auto-Queue.
*/
ProductionQueue.prototype.EnableAutoQueue = function()
{
this.autoqueuing = true;
};
/**
* Turn off Auto-Queue.
*/
ProductionQueue.prototype.DisableAutoQueue = function()
{
delete this.autoqueuing;
};
/**
* Calculate the new list of producible entities
* and update any entities currently being produced.
*/
ProductionQueue.prototype.CalculateEntitiesMap = function()
{
// Don't reset the map, it's used below to update entities.
if (!this.entitiesMap)
this.entitiesMap = new Map();
if (!this.template.Entities)
return;
let string = this.template.Entities._string;
// Tokens can be added -> process an empty list to get them.
let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity);
if (!addedTokens && !string)
return;
addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpPlayer = QueryOwnerInterface(this.entity);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
/**
* Process tokens:
* - process token modifiers (this is a bit tricky).
* - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
* - remove disabled entities
* - upgrade templates where necessary
* This also updates currently queued production (it's more convenient to do it here).
*/
let removeAllQueuedTemplate = (token) => {
let queue = clone(this.queue);
let template = this.entitiesMap.get(token);
for (let item of queue)
if (item.entity?.template && item.entity.template === template)
this.RemoveItem(item.id);
};
let updateAllQueuedTemplate = (token, updateTo) => {
let template = this.entitiesMap.get(token);
for (let item of this.queue)
if (item.entity?.template && item.entity.template === template)
item.entity.template = updateTo;
};
let toks = string.split(/\s+/);
for (let tok of addedTokens)
toks.push(tok);
let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
this.entitiesMap = toks.reduce((entMap, token) => {
let rawToken = token;
if (!(token in addedDict))
{
// This is a bit wasteful but I can't think of a simpler/better way.
// The list of token is unlikely to be a performance bottleneck anyways.
token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity);
token = token.split(/\s+/);
if (token.every(tok => addedTokens.indexOf(tok) !== -1))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = token[0];
}
// Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
if (cmpIdentity)
token = token.replace(/\{native\}/g, cmpIdentity.GetCiv());
if (cmpPlayer)
token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv());
// Filter out disabled and invalid entities.
if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = this.GetUpgradedTemplate(token);
entMap.set(rawToken, token);
updateAllQueuedTemplate(rawToken, token);
return entMap;
}, new Map());
};
/*
* Returns the upgraded template name if necessary.
*/
ProductionQueue.prototype.GetUpgradedTemplate = function(templateName)
{
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return templateName;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
while (template && template.Promotion !== undefined)
{
let requiredXp = ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
+template.Promotion.RequiredXp,
cmpPlayer.GetPlayerID(),
template);
if (requiredXp > 0)
break;
templateName = template.Promotion.Entity;
template = cmpTemplateManager.GetTemplate(templateName);
}
return templateName;
};
/*
* Returns list of technologies that can be researched by this building.
*/
ProductionQueue.prototype.GetTechnologiesList = function()
{
if (!this.template.Technologies)
return [];
let string = this.template.Technologies._string;
string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity);
if (!string)
return [];
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return [];
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return [];
let techs = string.split(/\s+/);
// Replace the civ specific technologies.
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let civTech = tech.replace("{civ}", cmpPlayer.GetCiv());
techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic");
}
// Remove any technologies that can't be researched by this civ.
techs = techs.filter(tech =>
cmpTechnologyManager.CheckTechnologyRequirements(
DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()),
true));
let techList = [];
// Stores the tech which supersedes the key.
let superseded = {};
let disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
// Add any top level technologies to an array which corresponds to the displayed icons.
// Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }.
for (let tech of techs)
{
if (disabledTechnologies && disabledTechnologies[tech])
continue;
let template = TechnologyTemplates.Get(tech);
if (!template.supersedes || techs.indexOf(template.supersedes) === -1)
techList.push(tech);
else
superseded[template.supersedes] = tech;
}
// Now make researched/in progress techs invisible.
for (let i in techList)
{
let tech = techList[i];
while (this.IsTechnologyResearchedOrInProgress(tech))
tech = superseded[tech];
techList[i] = tech;
}
let ret = [];
// This inserts the techs into the correct positions to line up the technology pairs.
for (let i = 0; i < techList.length; ++i)
{
let tech = techList[i];
if (!tech)
{
ret[i] = undefined;
continue;
}
let template = TechnologyTemplates.Get(tech);
if (template.top)
ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom };
else
ret[i] = tech;
}
return ret;
};
ProductionQueue.prototype.GetTechCostMultiplier = function()
{
let techCostMultiplier = {};
for (let res in this.template.TechCostMultiplier)
techCostMultiplier[res] = ApplyValueModificationsToEntity(
"ProductionQueue/TechCostMultiplier/" + res,
+this.template.TechCostMultiplier[res],
this.entity);
return techCostMultiplier;
};
ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech)
{
if (!tech)
return false;
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
let template = TechnologyTemplates.Get(tech);
if (template.top)
return cmpTechnologyManager.IsTechnologyResearched(template.top) ||
cmpTechnologyManager.IsInProgress(template.top) ||
cmpTechnologyManager.IsTechnologyResearched(template.bottom) ||
cmpTechnologyManager.IsInProgress(template.bottom);
return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech);
};
/*
* Adds a new batch of identical units to train or a technology to research to the production queue.
* @param {string} templateName - The template to start production on.
* @param {string} type - The type of production (i.e. "unit" or "technology").
* @param {number} count - The amount of units to be produced. Ignored for a tech.
* @param {any} metadata - Optionally any metadata to be attached to the item.
+ * @param {boolean} pushFront - Whether to push the item to the front of the queue and pause any item(s) currently in progress.
*
* @return {boolean} - Whether the addition of the item has succeeded.
*/
-ProductionQueue.prototype.AddItem = function(templateName, type, count, metadata)
+ProductionQueue.prototype.AddItem = function(templateName, type, count, metadata, pushFront = false)
{
// TODO: there should be a way for the GUI to determine whether it's going
// to be possible to add a batch (based on resource costs and length limits).
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return false;
let player = cmpPlayer.GetPlayerID();
if (!this.queue.length)
{
let cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade);
if (cmpUpgrade && cmpUpgrade.IsUpgrading())
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("Entity is being upgraded. Cannot start production."),
"translateMessage": true
});
return false;
}
}
else if (this.queue.length >= this.MaxQueueSize)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("The production queue is full."),
"translateMessage": true,
});
return false;
}
const item = {
"player": player,
"metadata": metadata,
"productionStarted": false,
"resources": {}, // The total resource costs.
+ "paused": false
};
// ToDo: Still some duplication here, some can might be combined,
// but requires some more refactoring.
if (type == "unit")
{
if (!Number.isInteger(count) || count <= 0)
{
error("Invalid batch count " + count);
return false;
}
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(this.GetUpgradedTemplate(templateName));
if (!template)
return false;
item.entity = {
"template": templateName,
"count": count,
"population": ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
player,
template),
"resources": {}, // The resource costs per entity.
};
for (let res in template.Cost.Resources)
{
item.entity.resources[res] = ApplyValueModificationsToTemplate(
"Cost/Resources/" + res,
+template.Cost.Resources[res],
player,
template);
item.resources[res] = Math.floor(count * item.entity.resources[res]);
}
if (template.TrainingRestrictions)
{
let unitCategory = template.TrainingRestrictions.Category;
let cmpPlayerEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
{
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, count, templateName, template.TrainingRestrictions.MatchLimit))
// Already warned, return.
return false;
cmpPlayerEntityLimits.ChangeCount(unitCategory, count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(templateName, count);
}
}
const buildTime = ApplyValueModificationsToTemplate(
"Cost/BuildTime",
+template.Cost.BuildTime,
player,
template);
const time = this.GetBatchTime(count) * buildTime * 1000;
item.timeTotal = time;
item.timeRemaining = time;
}
else if (type == "technology")
{
if (!TechnologyTemplates.Has(templateName))
return false;
if (!this.GetTechnologiesList().some(tech =>
tech &&
(tech == templateName ||
tech.pair &&
(tech.top == templateName || tech.bottom == templateName))))
{
error("This entity cannot research " + templateName);
return false;
}
item.technology = {
"template": templateName,
"resources": {}
};
let template = TechnologyTemplates.Get(templateName);
let techCostMultiplier = this.GetTechCostMultiplier();
if (template.cost)
for (const res in template.cost)
{
item.technology.resources[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]);
item.resources[res] = item.technology.resources[res];
}
const time = techCostMultiplier.time * (template.researchTime || 0) * 1000;
item.timeTotal = time;
item.timeRemaining = time;
}
else
{
warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue");
return false;
}
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer.TrySubtractResources(item.resources))
return false;
item.id = this.nextID++;
- this.queue.push(item);
+ if (pushFront)
+ {
+ if (this.queue[0])
+ this.queue[0].paused = true;
+ this.queue.unshift(item);
+ }
+ else
+ this.queue.push(item);
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (item.entity)
cmpTrigger.CallEvent("OnTrainingQueued", {
"playerid": player,
"unitTemplate": item.entity.template,
"count": count,
"metadata": metadata,
"trainerEntity": this.entity
});
if (item.technology)
{
// Tell the technology manager that we have started researching this
// such that players can't research the same thing twice.
const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.QueuedResearch(templateName, this.entity);
cmpTrigger.CallEvent("OnResearchQueued", {
"playerid": player,
"technologyTemplate": item.technology.template,
"researcherEntity": this.entity
});
}
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
if (!this.timer)
this.StartTimer();
return true;
};
/*
* Removes an item from the queue.
* Refunds resource costs and population reservations.
* item.player is used as this.entity's owner may have changed.
*/
ProductionQueue.prototype.RemoveItem = function(id)
{
let itemIndex = this.queue.findIndex(item => item.id == id);
if (itemIndex == -1)
return;
let item = this.queue[itemIndex];
// Destroy any cached entities (those which didn't spawn for some reason).
if (item.entity?.cache?.length)
{
for (const ent of item.entity.cache)
Engine.DestroyEntity(ent);
delete item.entity.cache;
}
const cmpPlayer = QueryPlayerIDInterface(item.player);
if (item.entity)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
const template = cmpTemplateManager.GetTemplate(item.entity.template);
if (template.TrainingRestrictions)
{
let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.entity.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(item.entity.template, -item.entity.count);
}
if (cmpPlayer)
{
if (item.productionStarted)
cmpPlayer.UnReservePopulationSlots(item.entity.population * item.entity.count);
if (itemIndex == 0)
cmpPlayer.UnBlockTraining();
}
}
let cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
const totalCosts = {};
for (let resource in item.resources)
{
totalCosts[resource] = 0;
if (item.entity)
totalCosts[resource] += Math.floor(item.entity.count * item.entity.resources[resource]);
if (item.technology)
totalCosts[resource] += item.technology.resources[resource];
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]);
}
if (cmpPlayer)
cmpPlayer.AddResources(totalCosts);
if (item.technology)
{
let cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.StoppedResearch(item.technology.template, true);
}
this.queue.splice(itemIndex, 1);
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
if (!this.queue.length)
this.StopTimer();
};
ProductionQueue.prototype.SetAnimation = function(name)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation(name, false, 1);
};
/*
* Returns basic data from all batches in the production queue.
*/
ProductionQueue.prototype.GetQueue = function()
{
return this.queue.map(item => ({
"id": item.id,
"unitTemplate": item.entity?.template,
"technologyTemplate": item.technology?.template,
"count": item.entity?.count,
"neededSlots": item.entity?.neededSlots,
"progress": 1 - (item.timeRemaining / (item.timeTotal || 1)),
"timeRemaining": item.timeRemaining,
+ "paused": item.paused,
"metadata": item.metadata
}));
};
/*
* Removes all existing batches from the queue.
*/
ProductionQueue.prototype.ResetQueue = function()
{
while (this.queue.length)
this.RemoveItem(this.queue[0].id);
this.DisableAutoQueue();
};
/*
* Returns batch build time.
*/
ProductionQueue.prototype.GetBatchTime = function(batchSize)
{
// TODO: work out what equation we should use here.
return Math.pow(batchSize, ApplyValueModificationsToEntity(
"ProductionQueue/BatchTimeModifier",
+this.template.BatchTimeModifier,
this.entity));
};
ProductionQueue.prototype.OnOwnershipChanged = function(msg)
{
// Reset the production queue whenever the owner changes.
// (This should prevent players getting surprised when they capture
// an enemy building, and then loads of the enemy's civ's soldiers get
// created from it. Also it means we don't have to worry about
// updating the reserved pop slots.)
this.ResetQueue();
if (msg.to != INVALID_PLAYER)
this.CalculateEntitiesMap();
};
ProductionQueue.prototype.OnCivChanged = function()
{
this.CalculateEntitiesMap();
};
/*
* This function creates the entities and places them in world if possible
* (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
* @param {Object} item - The item to spawn units for.
* @param {number} item.entity.count - The number of entities to spawn.
* @param {string} item.player - The owner of the item.
* @param {string} item.entity.template - The template to spawn.
* @param {any} - item.metadata - Optionally any metadata to add to the TrainingFinished message.
*
* @return {number} - The number of successfully created entities
*/
ProductionQueue.prototype.SpawnUnits = function(item)
{
let createdEnts = [];
let spawnedEnts = [];
// We need entities to test spawning, but we don't want to waste resources,
// so only create them once and use as needed.
if (!item.entity.cache)
{
item.entity.cache = [];
for (let i = 0; i < item.entity.count; ++i)
item.entity.cache.push(Engine.AddEntity(item.entity.template));
}
let autoGarrison;
let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
if (cmpRallyPoint)
{
let data = cmpRallyPoint.GetData()[0];
if (data && data.target && data.target == this.entity && data.command == "garrison")
autoGarrison = true;
}
let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let positionSelf = cmpPosition && cmpPosition.GetPosition();
let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
while (item.entity.cache.length)
{
const ent = item.entity.cache[0];
let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
let garrisoned = false;
if (autoGarrison)
{
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
{
// Temporary owner affectation needed for GarrisonHolder checks.
cmpNewOwnership.SetOwnerQuiet(item.player);
garrisoned = cmpGarrisonable.Garrison(this.entity);
cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
}
}
if (!garrisoned)
{
let pos = cmpFootprint.PickSpawnPoint(ent);
if (pos.y < 0)
break;
let cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
cmpNewPosition.JumpTo(pos.x, pos.z);
if (positionSelf)
cmpNewPosition.SetYRotation(positionSelf.horizAngleTo(pos));
spawnedEnts.push(ent);
}
// Decrement entity count in the EntityLimits component
// since it will be increased by EntityLimits.OnGlobalOwnershipChanged,
// i.e. we replace a 'trained' entity by 'alive' one.
// Must be done after spawn check so EntityLimits decrements only if unit spawns.
if (cmpPlayerEntityLimits)
{
let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
}
cmpNewOwnership.SetOwner(item.player);
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
item.entity.cache.shift();
createdEnts.push(ent);
}
if (spawnedEnts.length && !autoGarrison && cmpRallyPoint)
for (let com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
ProcessCommand(item.player, com);
if (createdEnts.length)
{
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
PlaySound("trained", createdEnts[0]);
Engine.PostMessage(this.entity, MT_TrainingFinished, {
"entities": createdEnts,
"owner": item.player,
"metadata": item.metadata
});
}
return createdEnts.length;
};
/*
* Increments progress on the first item in the production queue and blocks the
* queue if population limit is reached or some units failed to spawn.
* @param {Object} data - Unused in this case.
* @param {number} lateness - The time passed since the expected time to fire the function.
*/
ProductionQueue.prototype.ProgressTimeout = function(data, lateness)
{
if (this.paused)
return;
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
// Allocate available time to as many queue items as it takes
// until we've used up all the time (so that we work accurately
// with items that take fractions of a second).
let time = this.ProgressInterval + lateness;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
while (this.queue.length)
{
let item = this.queue[0];
+ if (item.paused)
+ item.paused = false;
if (!item.productionStarted)
{
if (item.entity)
{
const template = cmpTemplateManager.GetTemplate(item.entity.template);
item.entity.population = ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
item.player,
template);
item.entity.neededSlots = cmpPlayer.TryReservePopulationSlots(item.entity.population * item.entity.count);
if (item.entity.neededSlots)
{
cmpPlayer.BlockTraining();
return;
}
this.SetAnimation("training");
cmpPlayer.UnBlockTraining();
Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity });
}
if (item.technology)
{
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.StartedResearch(item.technology.template, true);
else
warn("Failed to start researching " + item.technology.template + ": No TechnologyManager available.");
this.SetAnimation("researching");
}
item.productionStarted = true;
}
if (item.timeRemaining > time)
{
item.timeRemaining -= time;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
return;
}
if (item.entity)
{
let numSpawned = this.SpawnUnits(item);
if (numSpawned)
cmpPlayer.UnReservePopulationSlots(item.entity.population * numSpawned);
if (numSpawned == item.entity.count)
{
cmpPlayer.UnBlockTraining();
delete this.spawnNotified;
}
else
{
if (numSpawned)
{
item.entity.count -= numSpawned;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
}
cmpPlayer.BlockTraining();
if (!this.spawnNotified)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Can't find free space to spawn trained units"),
"translateMessage": true
});
this.spawnNotified = true;
}
return;
}
}
if (item.technology)
{
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.ResearchTechnology(item.technology.template);
else
warn("Failed to finish researching " + item.technology.template + ": No TechnologyManager available.");
const template = TechnologyTemplates.Get(item.technology.template);
if (template && template.soundComplete)
{
let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager)
cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity);
}
}
time -= item.timeRemaining;
this.queue.shift();
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null);
// If autoqueuing, push a new unit on the queue immediately,
// but don't start right away. This 'wastes' some time, making
// autoqueue slightly worse than regular queuing, and also ensures
// that autoqueue doesn't train more than one item per turn,
// if the units would take fewer than ProgressInterval ms to train.
if (this.autoqueuing && item.entity)
{
if (!this.AddItem(item.entity.template, "unit", item.entity.count, item.metadata))
{
this.DisableAutoQueue();
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Could not auto-queue unit, de-activating."),
"translateMessage": true
});
}
break;
}
}
if (!this.queue.length)
this.StopTimer();
};
ProductionQueue.prototype.PauseProduction = function()
{
this.StopTimer();
this.paused = true;
+ if (this.queue[0])
+ this.queue[0].paused = true;
};
ProductionQueue.prototype.UnpauseProduction = function()
{
+ if (this.queue[0])
+ this.queue[0].paused = false;
delete this.paused;
this.StartTimer();
};
ProductionQueue.prototype.StartTimer = function()
{
if (this.timer)
return;
this.timer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetInterval(
this.entity,
IID_ProductionQueue,
"ProgressTimeout",
this.ProgressInterval,
this.ProgressInterval,
null
);
};
ProductionQueue.prototype.StopTimer = function()
{
if (!this.timer)
return;
this.SetAnimation("idle");
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.timer);
delete this.timer;
};
ProductionQueue.prototype.OnValueModification = function(msg)
{
// If the promotion requirements of units is changed,
// update the entities list so that automatically promoted units are shown
// appropriately in the list.
if (msg.component != "Promotion" && (msg.component != "ProductionQueue" ||
!msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/"))))
return;
if (msg.entities.indexOf(this.entity) === -1)
return;
// This also updates the queued production if necessary.
this.CalculateEntitiesMap();
// Inform the GUI that it'll need to recompute the selection panel.
// TODO: it would be better to only send the message if something actually changing
// for the current production queue.
let cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
};
ProductionQueue.prototype.HasQueuedProduction = function()
{
return this.queue.length > 0;
};
ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.CalculateEntitiesMap();
};
ProductionQueue.prototype.OnGarrisonedStateChanged = function(msg)
{
if (msg.holderID != INVALID_ENTITY)
this.PauseProduction();
else
this.UnpauseProduction();
};
Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25957)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25958)
@@ -1,1867 +1,1864 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return;
let data = {
"cmpPlayer": cmpPlayer,
"controlAllUnits": cmpPlayer.CanControlAllUnits()
};
if (cmd.entities)
data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
// TODO: queuing order and forcing formations doesn't really work.
// To play nice, we'll still no-formation queued order if units are in formation
// but the opposite perhaps ought to be implemented.
if (!cmd.queued || cmd.formation == NULL_FORMATION)
data.formation = cmd.formation || undefined;
// Allow focusing the camera on recent commands
let commandData = {
"type": "playercommand",
"players": [player],
"cmd": cmd
};
// Save the position, since the GUI event is received after the unit died
if (cmd.type == "delete-entities")
{
let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position);
commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D();
}
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification(commandData);
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
if (g_Commands[cmd.type])
{
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("OnPlayerCommand", { "player": player, "cmd": cmd });
g_Commands[cmd.type](player, cmd, data);
}
else
error("Invalid command: unknown command type: "+uneval(cmd));
}
var g_Commands = {
"aichat": function(player, cmd, data)
{
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
var notification = { "players": [player] };
for (var key in cmd)
notification[key] = cmd[key];
cmpGuiInterface.PushNotification(notification);
},
"cheat": function(player, cmd, data)
{
Cheat(cmd);
},
"collect-treasure": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CollectTreasure(cmd.target, cmd.queued);
});
},
"collect-treasure-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.queued);
});
},
"diplomacy": function(player, cmd, data)
{
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (data.cmpPlayer.GetLockTeams() ||
cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive())
return;
switch(cmd.to)
{
case "ally":
data.cmpPlayer.SetAlly(cmd.player);
break;
case "neutral":
data.cmpPlayer.SetNeutral(cmd.player);
break;
case "enemy":
data.cmpPlayer.SetEnemy(cmd.player);
break;
default:
warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "diplomacy",
"players": [player],
"targetPlayer": cmd.player,
"status": cmd.to
});
},
"tribute": function(player, cmd, data)
{
data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
},
"control-all": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - control all units)")
});
data.cmpPlayer.SetControlAllUnits(cmd.flag);
},
"reveal-map": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - reveal map)")
});
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
},
"walk": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront);
});
},
"walk-custom": function(player, cmd, data)
{
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront);
});
},
"walk-to-range": function(player, cmd, data)
{
// Only used by the AI
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront);
}
},
"attack-walk": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront);
});
},
"attack-walk-custom": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
for (let ent in data.entities)
GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront);
});
},
"attack": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
if (g_DebugCommands && !allowCapture &&
!(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target)))
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued, cmd.pushFront);
});
},
"patrol": function(player, cmd, data)
{
let allowCapture = cmd.allowCapture || cmd.allowCapture == null;
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued)
);
},
"heal": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront);
});
},
"repair": function(player, cmd, data)
{
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront);
});
},
"gather": function(player, cmd, data)
{
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront);
});
},
"gather-near-position": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront);
});
},
"returnresource": function(player, cmd, data)
{
if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront);
});
},
"back-to-work": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if(!cmpUnitAI || !cmpUnitAI.BackToWork())
notifyBackToWorkFailure(player);
}
},
"call-to-arms": function(player, cmd, data)
{
const unitsToMove = data.entities.filter(ent =>
MatchesClassList(Engine.QueryInterface(ent, IID_Identity).GetClassesList(),
["Soldier", "Warship", "Siege", "Healer"])
);
GetFormationUnitAIs(unitsToMove, player, cmd, data.formation).forEach(cmpUnitAI => {
const target = cmd.target;
if (cmd.pushFront)
{
cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, false, cmd.pushFront);
cmpUnitAI.DropAtNearestDropSite(false, cmd.pushFront);
}
else
{
cmpUnitAI.DropAtNearestDropSite(cmd.queued, false)
cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, true, false);
}
});
},
"remove-guard": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.RemoveGuard();
}
},
"train": function(player, cmd, data)
{
if (!Number.isInteger(cmd.count) || cmd.count <= 0)
{
warn("Invalid command: can't train " + uneval(cmd.count) + " units");
return;
}
// Check entity limits
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (data.entities.length <= 0)
{
if (g_DebugCommands)
warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
return;
}
for (let ent of data.entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
continue;
}
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
// Check if the building can train the unit
// TODO: the AI API does not take promotion technologies into account for the list
// of trainable units (taken directly from the unit template). Here is a temporary fix.
if (queue && data.cmpPlayer.IsAI())
{
var list = queue.GetEntitiesList();
if (list.indexOf(cmd.template) === -1 && cmd.promoted)
{
for (var promoted of cmd.promoted)
{
if (list.indexOf(promoted) === -1)
continue;
cmd.template = promoted;
break;
}
}
}
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
- if ("metadata" in cmd)
- queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata);
- else
- queue.AddItem(cmd.template, "unit", +cmd.count);
+ queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata, cmd.pushFront);
}
},
"research": function(player, cmd, data)
{
var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
return;
}
var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (queue)
- queue.AddItem(cmd.template, "technology");
+ queue.AddItem(cmd.template, "technology", undefined, cmd.metadata, cmd.pushFront);
},
"stop-production": function(player, cmd, data)
{
let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.RemoveItem(cmd.id);
},
"construct": function(player, cmd, data)
{
TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"construct-wall": function(player, cmd, data)
{
TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
},
"delete-entities": function(player, cmd, data)
{
for (let ent of data.entities)
{
if (!data.controlAllUnits)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity && cmpIdentity.IsUndeletable())
continue;
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable &&
cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2)
continue;
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather())
continue;
}
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
{
let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health);
if (cmpMiragedHealth)
cmpMiragedHealth.Kill();
else
Engine.DestroyEntity(cmpMirage.parent);
Engine.DestroyEntity(ent);
continue;
}
let cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(ent);
}
},
"set-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(clone(cmd.data));
}
}
},
"unset-rallypoint": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
}
},
"resign": function(player, cmd, data)
{
data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned."));
},
"occupy-turret": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.OccupyTurret(cmd.target, cmd.queued);
});
},
"garrison": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront);
});
},
"guard": function(player, cmd, data)
{
if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd));
return;
}
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront);
});
},
"stop": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.Stop(cmd.queued);
});
},
"leave-turret": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"unload-turrets": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
for (let turret of cmpTurretHolder.GetEntities())
{
let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"unload": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits))
{
if (g_DebugCommands)
warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
return;
}
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
var notUngarrisoned = 0;
// The owner can ungarrison every garrisoned unit
if (IsOwnedByPlayer(player, cmd.garrisonHolder))
data.entities = cmd.entities;
for (let ent of data.entities)
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
++notUngarrisoned;
if (notUngarrisoned != 0)
notifyUnloadFailure(player, cmd.garrisonHolder);
},
"unload-template": function(player, cmd, data)
{
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
// Only the owner of the garrisonHolder may unload entities from any owners
if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
&& player != +cmd.owner)
continue;
if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all))
notifyUnloadFailure(player, garrisonHolder);
}
}
},
"unload-all-by-owner": function(player, cmd, data)
{
var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player))
notifyUnloadFailure(player, garrisonHolder);
}
},
"unload-all": function(player, cmd, data)
{
var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
for (let garrisonHolder of entities)
{
var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
notifyUnloadFailure(player, garrisonHolder);
}
},
"alert-raise": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.RaiseAlert();
}
},
"alert-end": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
cmpAlertRaiser.EndOfAlert();
}
},
"formation": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => {
cmpUnitAI.MoveIntoFormation(cmd);
});
},
"promote": function(player, cmd, data)
{
if (!data.cmpPlayer.GetCheatsEnabled())
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": markForTranslation("(Cheat - promoted units)"),
"translateMessage": true
});
for (let ent of cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
},
"stance": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && !cmpUnitAI.IsTurret())
cmpUnitAI.SwitchToStance(cmd.name);
}
},
"lock-gate": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (!cmpGate)
continue;
if (cmd.lock)
cmpGate.LockGate();
else
cmpGate.UnlockGate();
}
},
"setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
});
},
"cancel-setup-trade-route": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CancelSetupTradeRoute(cmd.target);
});
},
"set-trading-goods": function(player, cmd, data)
{
data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
},
"barter": function(player, cmd, data)
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount);
},
"set-shading-color": function(player, cmd, data)
{
// Prevent multiplayer abuse
if (!data.cmpPlayer.IsAI())
return;
// Debug command to make an entity brightly colored
for (let ent of cmd.entities)
{
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0
}
},
"pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.Pack(cmd.queued, cmd.pushFront);
else
cmpUnitAI.Unpack(cmd.queued, cmd.pushFront);
}
},
"cancel-pack": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
if (cmd.pack)
cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront);
else
cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront);
}
},
"upgrade": function(player, cmd, data)
{
for (let ent of data.entities)
{
var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template))
continue;
if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template))
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [player],
"message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.")
});
continue;
}
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd));
continue;
}
let cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
let requiredTechnology = cmpUpgrade.GetRequiredTechnology(cmd.template);
if (requiredTechnology && (!cmpTechnologyManager || !cmpTechnologyManager.IsTechnologyResearched(requiredTechnology)))
{
if (g_DebugCommands)
warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd));
continue;
}
cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer);
}
},
"cancel-upgrade": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
cmpUpgrade.CancelUpgrade(player);
}
},
"attack-request": function(player, cmd, data)
{
// Send a chat message to human players
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "aichat",
"players": [player],
"message": "/allies " + markForTranslation("Attack against %(_player_)s requested."),
"translateParameters": ["_player_"],
"parameters": { "_player_": cmd.player }
});
// And send an attackRequest event to the AIs
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("AttackRequest", cmd);
},
"spy-request": function(player, cmd, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => {
let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing);
return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player);
}));
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "spy-response",
"players": [player],
"target": cmd.player,
"entity": ent
});
if (ent)
Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source);
else
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy");
IncurBribeCost(template, player, cmd.player, true);
// update statistics for failed bribes
let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
if (cmpBribesStatisticsTracker)
cmpBribesStatisticsTracker.IncreaseFailedBribesCounter();
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("There are no bribable units"),
"translateMessage": true
});
}
},
"diplomacy-request": function(player, cmd, data)
{
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("DiplomacyRequest", cmd);
},
"tribute-request": function(player, cmd, data)
{
let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
if (cmpAIInterface)
cmpAIInterface.PushEvent("TributeRequest", cmd);
},
"dialog-answer": function(player, cmd, data)
{
// Currently nothing. Triggers can read it anyway, and send this
// message to any component you like.
},
"set-dropsite-sharing": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite && cmpResourceDropsite.IsSharable())
cmpResourceDropsite.SetSharing(cmd.shared);
}
},
"map-flare": function(player, cmd, data)
{
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "map-flare",
"players": [player],
"target": cmd.target
});
},
"autoqueue-on": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.EnableAutoQueue();
}
},
"autoqueue-off": function(player, cmd, data)
{
for (let ent of data.entities)
{
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.DisableAutoQueue();
}
},
};
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Unable to unload unit(s)."),
"translateMessage": true
});
}
/**
* Sends a GUI notification about worker(s) that failed to go back to work.
*/
function notifyBackToWorkFailure(player)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Some unit(s) can't go back to work"),
"translateMessage": true
});
}
/**
* Sends a GUI notification about entities that can't be controlled.
* @param {number} player - The player-ID of the player that needs to receive this message.
*/
function notifyOrderFailure(entity, player)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity)
return;
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("%(unit)s can't be controlled."),
"parameters": { "unit": cmpIdentity.GetGenericName() },
"translateParameters": ["unit"],
"translateMessage": true
});
}
/**
* Get some information about the formations used by entities.
*/
function ExtractFormations(ents)
{
let entities = []; // Entities with UnitAI.
let members = {}; // { formationentity: [ent, ent, ...], ... }
let templates = {}; // { formationentity: template }
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
entities.push(ent);
let fid = cmpUnitAI.GetFormationController();
if (fid == INVALID_ENTITY)
continue;
if (!members[fid])
{
members[fid] = [];
templates[fid] = cmpUnitAI.GetFormationTemplate();
}
members[fid].push(ent);
}
return {
"entities": entities,
"members": members,
"templates": templates
};
}
/**
* Tries to find the best angle to put a dock at a given position
* Taken from GuiInterface.js
*/
function GetDockAngle(template, x, z)
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
return undefined;
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
else if (template.Footprint.Circle)
halfSize = template.Footprint.Circle["@radius"];
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = x - d*Math.sin(angle);
var nz = z + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
waterPoints.push(i);
}
var consec = [];
var length = waterPoints.length;
if (!length)
continue;
for (var i = 0; i < length; ++i)
{
var count = 0;
for (let j = 0; j < length - 1; ++j)
{
if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length])
++count;
else
break;
}
consec[i] = count;
}
var start = 0;
var count = 0;
for (var c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI;
}
return undefined;
}
/**
* Attempts to construct a building using the specified parameters.
* Returns true on success, false on failure.
*/
function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
{
// Message structure:
// {
// "type": "construct",
// "entities": [...], // entities that will be ordered to construct the building (if applicable)
// "template": "...", // template name of the entity being constructed
// "x": ...,
// "z": ...,
// "angle": ...,
// "metadata": "...", // AI metadata of the building
// "actorSeed": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
// "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
// "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
// // testing to determine placement validity. May be INVALID_ENTITY.
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check whether we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
return false;
var foundationTemplate = "foundation|" + cmd.template;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity(foundationTemplate);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
return false;
}
// If it's a dock, get the right angle.
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
var angle = cmd.angle;
if (template.BuildRestrictions.PlacementType === "shore")
{
let angleDock = GetDockAngle(template, cmd.x, cmd.z);
if (angleDock !== undefined)
angle = angleDock;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(angle);
// Set the obstruction control group if needed
if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
{
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
// primary control group must always be valid
if (cmd.obstructionControlGroup)
{
if (cmd.obstructionControlGroup <= 0)
warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
}
if (cmd.obstructionControlGroup2)
cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (cmpBuildRestrictions)
{
var ret = cmpBuildRestrictions.CheckPlacement();
if (!ret.success)
{
if (g_DebugCommands)
warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
ret.players = [player];
cmpGuiInterface.PushNotification(ret);
// Remove the foundation because the construction was aborted
// move it out of world because it's not destroyed immediately.
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
}
else
error("cmpBuildRestrictions not defined");
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
return false;
}
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
{
if (g_DebugCommands)
warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("The building's technology requirements are not met."),
"translateMessage": true
});
// Remove the foundation because the construction was aborted
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
}
// We need the cost after tech and aura modifications.
let cmpCost = Engine.QueryInterface(ent, IID_Cost);
let costs = cmpCost.GetResourceCosts();
if (!cmpPlayer.TrySubtractResources(costs))
{
if (g_DebugCommands)
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
Engine.DestroyEntity(ent);
cmpPosition.MoveOutOfWorld();
return false;
}
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(cmd.template);
// send Metadata info if any
if (cmd.metadata)
Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued,
"pushFront": cmd.pushFront,
"formation": cmd.formation || undefined
});
}
return ent;
}
function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
{
// 'cmd' message structure:
// {
// "type": "construct-wall",
// "entities": [...], // entities that will be ordered to construct the wall (if applicable)
// "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
// {
// "template": "...", // one of the templates from the wallset
// "x": ...,
// "z": ...,
// "angle": ...,
// },
// ...
// ],
// "wallSet": {
// "templates": {
// "tower": // tower template name
// "long": // long wall segment template name
// ... // etc.
// },
// "maxTowerOverlap": ...,
// "minTowerOverlap": ...,
// },
// "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
// "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
// }
if (cmd.pieces.length <= 0)
return;
if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
return;
}
if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
{
error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
return;
}
// Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
// and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
// groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
// towers in the case of snapping). The towers themselves all keep their default unique control groups.
// To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
// it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
// first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
// the first tower encountered towards the ending side of the wall (if any).
// We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
// wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
//
// FIRST PASS:
// - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
// cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
// as the primary control group, thus allowing it to be built overlapping the previous piece.
// - If we encounter a new tower along the way (which will gain its own control group), do the following:
// o First build it using temporarily the same control group of the previous (non-tower) piece
// o Set the previous piece's secondary control group to the tower's entity ID
// o Restore the primary control group of the constructed tower back its original (unique) value.
// The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
// to be placed while overlapping the previous piece.
//
// SECOND PASS:
// - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
// time registering the right neighbouring tower in each non-tower piece.
// first pass; L -> R
var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
// the first wall piece can be built while overlapping it.
if (cmd.startSnappedEntity)
{
var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
if (!cmpSnappedStartObstruction)
{
error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
//warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
}
var i = 0;
var queued = cmd.queued;
var pieces = clone(cmd.pieces);
for (; i < pieces.length; ++i)
{
var piece = pieces[i];
// All wall pieces after the first must be queued.
if (i > 0 && !queued)
queued = true;
// 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
// start position snapping (implying that the first entity we build must be a tower)
if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
{
if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
{
error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
break;
}
}
var constructPieceCmd = {
"type": "construct",
"entities": cmd.entities,
"template": piece.template,
"x": piece.x,
"z": piece.z,
"angle": piece.angle,
"autorepair": cmd.autorepair,
"autocontinue": cmd.autocontinue,
"queued": queued,
// Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
// using the control group of the last tower (see comments above).
"obstructionControlGroup": lastTowerControlGroup,
};
// If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
// control group directly at construction time (instead of setting it in the second pass) to allow it to be built
// while overlapping the snapped entity.
if (i == pieces.length - 1 && cmd.endSnappedEntity)
{
var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (cmpEndSnappedObstruction)
constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
}
var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
if (pieceEntityId)
{
// wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
piece.ent = pieceEntityId;
// if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
if (piece.template == cmd.wallSet.templates.tower)
{
var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
var newTowerControlGroup = pieceEntityId;
if (i > 0)
{
//warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction);
// TODO: ensure that cmpPreviousObstruction exists
// TODO: ensure that the previous obstruction does not yet have a secondary control group set
cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
}
// TODO: ensure that cmpTowerObstruction exists
cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
lastTowerIndex = i;
lastTowerControlGroup = newTowerControlGroup;
}
}
else // failed to build wall piece, abort
break;
}
var lastBuiltPieceIndex = i - 1;
var wallComplete = (lastBuiltPieceIndex == pieces.length - 1);
// At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
// Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
// as their secondary control groups.
lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
// only start off with the ending side's snapped tower's control group if we were able to build the entire wall
if (cmd.endSnappedEntity && wallComplete)
{
var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
if (!cmpSnappedEndObstruction)
{
error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
return;
}
lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
}
for (var j = lastBuiltPieceIndex; j >= 0; --j)
{
var piece = pieces[j];
if (!piece.ent)
{
error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
continue;
}
var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
if (!cmpPieceObstruction)
{
error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
continue;
}
if (piece.template == cmd.wallSet.templates.tower)
{
// encountered a tower entity, update the last tower control group
lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
}
else
{
// Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
// Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
// dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
if (existingSecondaryControlGroup == INVALID_ENTITY)
{
if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
{
cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
}
}
else if (existingSecondaryControlGroup != lastTowerControlGroup)
{
error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
break;
}
}
}
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
let formation = ExtractFormations(ents);
for (let fid in formation.members)
{
let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate)
{
// If an individual was selected, remove it from any formation
// and command it individually.
if (ents.length == 1)
{
let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
let formationUnitAIs = [];
// Find what formations the selected entities are currently in,
// and default to that unless the formation is forced or it's the null formation
// (we want that to reset whatever formations units are in).
if (formationTemplate != NULL_FORMATION)
{
let formation = ExtractFormations(ents);
let formationIds = Object.keys(formation.members);
if (formationIds.length == 1)
{
// Selected units either belong to this formation or have no formation.
let fid = formationIds[0];
let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length &&
cmpFormation.GetMemberCount() == formation.entities.length)
{
cmpFormation.DeleteTwinFormations();
// The whole formation was selected, so reuse its controller for this command.
if (!forceTemplate || formationTemplate == formation.templates[fid])
{
formationTemplate = formation.templates[fid];
formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
}
else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)];
}
else if (cmpFormation && !forceTemplate)
{
// Just reuse the template.
formationTemplate = formation.templates[fid];
}
}
else if (formationIds.length)
{
// Check if all entities share a common formation, if so reuse this template.
let template = formation.templates[formationIds[0]];
for (let i = 1; i < formationIds.length; ++i)
if (formation.templates[formationIds[i]] != template)
{
template = null;
break;
}
if (template && !forceTemplate)
formationTemplate = template;
}
}
// Separate out the units that don't support the chosen formation.
let formedUnits = [];
let nonformedUnitAIs = [];
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: We only check if the formation is usable by some units
// if we move them to it. We should check if we can use formations
// for the other cases.
let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION;
if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION))
{
if (nullFormation && cmpUnitAI.GetFormationController())
cmpUnitAI.LeaveFormation(cmd.queued || false);
nonformedUnitAIs.push(cmpUnitAI);
}
else
formedUnits.push(ent);
}
if (nonformedUnitAIs.length == ents.length)
{
// No units support the formation.
return nonformedUnitAIs;
}
if (!formationUnitAIs.length)
{
// We need to give the selected units a new formation controller.
// TODO replace the fixed 60 with something sensible, based on vision range f.e.
let formationSeparation = 60;
let clusters = ClusterEntities(formedUnits, formationSeparation);
let formationEnts = [];
for (let cluster of clusters)
{
RemoveFromFormation(cluster);
if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
{
for (let ent of cluster)
nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI));
continue;
}
// Create the new controller.
let formationEnt = Engine.AddEntity(formationTemplate);
let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
cmpFormation.SetFormationSeparation(formationSeparation);
cmpFormation.SetMembers(cluster);
for (let ent of formationEnts)
cmpFormation.RegisterTwinFormation(ent);
formationEnts.push(formationEnt);
let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
cmpOwnership.SetOwner(player);
}
}
return nonformedUnitAIs.concat(formationUnitAIs);
}
/**
* Group a list of entities in clusters via single-links
*/
function ClusterEntities(ents, separationDistance)
{
let clusters = [];
if (!ents.length)
return clusters;
let distSq = separationDistance * separationDistance;
let positions = [];
// triangular matrix with the (squared) distances between the different clusters
// the other half is not initialised
let matrix = [];
for (let i = 0; i < ents.length; ++i)
{
matrix[i] = [];
clusters.push([ents[i]]);
let cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
positions.push(cmpPosition.GetPosition2D());
for (let j = 0; j < i; ++j)
matrix[i][j] = positions[i].distanceToSquared(positions[j]);
}
while (clusters.length > 1)
{
// search two clusters that are closer than the required distance
let closeClusters = undefined;
for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i)
for (let j = i - 1; j >= 0 && !closeClusters; --j)
if (matrix[i][j] < distSq)
closeClusters = [i,j];
// if no more close clusters found, just return all found clusters so far
if (!closeClusters)
return clusters;
// make a new cluster with the entities from the two found clusters
let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
// calculate the minimum distance between the new cluster and all other remaining
// clusters by taking the minimum of the two distances.
let distances = [];
for (let i = 0; i < clusters.length; ++i)
{
let a = closeClusters[1];
let b = closeClusters[0];
if (i == a || i == b)
continue;
let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a];
let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b];
distances.push(Math.min(dist1, dist2));
}
// remove the rows and columns in the matrix for the merged clusters,
// and the clusters themselves from the cluster list
clusters.splice(closeClusters[0],1);
clusters.splice(closeClusters[1],1);
matrix.splice(closeClusters[0],1);
matrix.splice(closeClusters[1],1);
for (let i = 0; i < matrix.length; ++i)
{
if (matrix[i].length > closeClusters[0])
matrix[i].splice(closeClusters[0],1);
if (matrix[i].length > closeClusters[1])
matrix[i].splice(closeClusters[1],1);
}
// add a new row of distances to the matrix and the new cluster
clusters.push(newCluster);
matrix.push(distances);
}
return clusters;
}
function GetFormationRequirements(formationTemplate)
{
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate);
if (!template.Formation)
return false;
return { "minCount": +template.Formation.RequiredMemberCount };
}
function CanMoveEntsIntoFormation(ents, formationTemplate)
{
// TODO: should check the player's civ is allowed to use this formation
// See simulation/components/Player.js GetFormations() for a list of all allowed formations
var requirements = GetFormationRequirements(formationTemplate);
if (!requirements)
return false;
var count = 0;
for (let ent of ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate))
continue;
++count;
}
return count >= requirements.minCount;
}
/**
* Check if player can control this entity
* returns: true if the entity is owned by the player and controllable
* or control all units is activated, else false
*/
function CanControlUnit(entity, player, controlAll)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
let canBeControlled = IsOwnedByPlayer(player, entity) &&
(!cmpIdentity || cmpIdentity.IsControllable()) ||
controlAll;
if (!canBeControlled)
notifyOrderFailure(entity, player);
return canBeControlled;
}
/**
* @param {number} entity - The entityID to verify.
* @param {number} player - The playerID to check against.
* @return {boolean}.
*/
function IsOwnedByPlayerOrMutualAlly(entity, player)
{
return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity);
}
/**
* Check if player can control this entity
* @return {boolean} - True if the entity is valid and controlled by the player
* or the entity is owned by an mutualAlly and can be controlled
* or control all units is activated, else false.
*/
function CanPlayerOrAllyControlUnit(entity, player, controlAll)
{
return CanControlUnit(player, entity, controlAll) ||
IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity);
}
/**
* @return {boolean} - Whether the owner of this entity can control the entity.
*/
function CanOwnerControlEntity(entity)
{
let cmpOwner = QueryOwnerInterface(entity);
return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID());
}
/**
* Filter entities which the player can control.
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(ent => CanControlUnit(ent, player, controlAll));
}
/**
* Filter entities which the player can control or are mutualAlly
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll));
}
/**
* Incur the player with the cost of a bribe, optionally multiply the cost with
* the additionalMultiplier
*/
function IncurBribeCost(template, player, playerBribed, failedBribe)
{
let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed);
if (!cmpPlayerBribed)
return false;
let costs = {};
// Additional cost for this owner
let multiplier = cmpPlayerBribed.GetSpyCostMultiplier();
if (failedBribe)
multiplier *= template.VisionSharing.FailureCostRatio;
for (let res in template.Cost.Resources)
costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template));
let cmpPlayer = QueryPlayerIDInterface(player);
return cmpPlayer && cmpPlayer.TrySubtractResources(costs);
}
Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Engine.RegisterGlobal("g_Commands", g_Commands);
Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost);