Index: ps/trunk/binaries/data/mods/public/globalscripts/l10n.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/l10n.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/globalscripts/l10n.js (revision 15375)
@@ -1,213 +1,218 @@
var g_translations = {};
var g_pluralTranslations = {};
var g_translationsWithContext = {};
var g_pluralTranslationsWithContext = {};
// Checks if the specified variable is a string, and if it is, it checks that it
// is not empty.
function isNonEmptyString(variable)
{
if (typeof variable != "string")
return false;
else if (variable.trim())
return true;
else
return false;
}
// Translates the specified English message into the current language.
//
// This function relies on the g_translations cache when possible. You should use this function instead of
// Engine.Translate() whenever you can to minimize the number of C++ calls and string conversions involved.
function translate(message)
{
var translation = g_translations[message];
if (!translation)
return g_translations[message] = Engine.Translate(message);
return translation;
}
// Translates the specified English message into the current language for the specified number.
//
// This function relies on the g_pluralTranslations cache when possible. You should use this function instead of
// Engine.TranslatePlural() whenever you can to minimize the number of C++ calls and string conversions involved.
function translatePlural(singularMessage, pluralMessage, number)
{
var translation = g_pluralTranslations[singularMessage];
if (!translation)
g_pluralTranslations[singularMessage] = {};
var pluralTranslation = g_pluralTranslations[singularMessage][number];
if (!pluralTranslation)
return g_pluralTranslations[singularMessage][number] = Engine.TranslatePlural(singularMessage, pluralMessage, number);
return pluralTranslation;
}
// Translates the specified English message into the current language for the specified context.
//
// This function relies on the g_translationsWithContext cache when possible. You should use this function instead of
// Engine.TranslateWithContext() whenever you can to minimize the number of C++ calls and string conversions involved.
function translateWithContext(context, message)
{
var translationContext = g_translationsWithContext[context];
if (!translationContext)
g_translationsWithContext[context] = {}
var translationWithContext = g_translationsWithContext[context][message];
if (!translationWithContext)
return g_translationsWithContext[context][message] = Engine.TranslateWithContext(context, message);
return translationWithContext;
}
// Translates the specified English message into the current language for the specified context and number.
//
// This function relies on the g_pluralTranslationsWithContext cache when possible. You should use this function instead of
// Engine.TranslatePluralWithContext() whenever you can to minimize the number of C++ calls and string conversions involved.
function translatePluralWithContext(context, singularMessage, pluralMessage, number)
{
var translationContext = g_pluralTranslationsWithContext[context];
if (!translationContext)
g_pluralTranslationsWithContext[context] = {};
var translationWithContext = g_pluralTranslationsWithContext[context][singularMessage];
if (!translationWithContext)
g_pluralTranslationsWithContext[context][singularMessage] = {};
var pluralTranslationWithContext = g_pluralTranslationsWithContext[context][singularMessage][number];
if (!pluralTranslationWithContext)
return g_pluralTranslationsWithContext[context][singularMessage][number] = Engine.TranslatePluralWithContext(context, singularMessage, pluralMessage, number);
return pluralTranslationWithContext;
}
/**
* The input object should contain either of the following properties:
*
* • A ‘message’ property that contains a message to translate.
*
* • A ‘list’ property that contains a list of messages to translate as a
* comma-separated list of translated.
*
* Optionally, the input object may contain a ‘context’ property. In that case,
* the value of this property is used as translation context, that is, passed to
* the translateWithContext(context, message) function.
*/
function translateMessageObject(object)
{
// the translation function
var trans = translate;
if (object.context)
trans = function(msg) { return translateWithContext(object.context, msg);};
if (object.message)
object = trans(object.message);
else if (object.list)
{
var translatedList = object.list.map(trans);
object = translatedList.join(translateWithContext("enumeration", ", "));
}
return object;
}
/**
* Translates any string value in the specified JavaScript object
* that is associated with a key included in the specified keys array.
*
* it accepts an object in the form of
*
* {
* translatedString1: "my first message",
* unTranslatedString1: "some English string",
* ignoredObject: {
* translatedString2: "my second message",
* unTranslatedString2: "some English string"
* },
* translatedObject1: {
* message: "my third singular message",
* context: "message context",
* },
* translatedObject2: {
* list: ["list", "of", "strings"],
* context: "message context",
* },
* }
*
* Together with a keys list to translate the strings and objects
* ["translatedString1", "translatedString2", "translatedObject1",
* "translatedObject2"]
*
* The result will be (f.e. in Dutch)
* {
* translatedString1: "mijn eerste bericht",
* unTranslatedString1: "some English string",
* ignoredObject: {
* translatedString2: "mijn tweede bericht",
* unTranslatedString2: "some English string"
* },
* translatedObject1: "mijn derde bericht",
* translatedObject2: "lijst, van, teksten",
* }
*
* So you see that the keys array can also contain lower-level keys,
* And that you can include objects in the keys array to translate
* them with a context, or to join a list of translations.
*
* Also, the keys array may be an object where properties are keys to translate
* and values are translation contexts to use for each key.
*/
function translateObjectKeys(object, keys) {
// If ‘keys’ is an array, simply translate.
if (keys instanceof Array)
{
for (var property in object)
{
if (keys.indexOf(property) > -1)
{
if (isNonEmptyString(object[property]))
object[property] = translate(object[property]);
else if (object[property] instanceof Object)
{
object[property] = translateMessageObject(object[property]);
}
}
else if (object[property] instanceof Object)
translateObjectKeys(object[property], keys);
}
}
// If ‘keys’ is not an array, it is an object where keys are properties to
// translate and values are translation contexts to use for each key.
// An empty value means no context.
else
{
for (var property in object)
{
if (property in keys)
{
if (isNonEmptyString(object[property]))
if (keys[property])
object[property] = translateWithContext(keys[property], object[property]);
else
object[property] = translate(object[property]);
else if (object[property] instanceof Object)
object[property] = translateMessageObject(object[property]);
}
else if (object[property] instanceof Object)
translateObjectKeys(object[property], keys);
}
}
}
+/**
+ * Function is used by the extract-messages tool.
+ * So it may only be used on a plain string,
+ * it won't have any effect on a calculated string.
+ */
function markForTranslation(message) {
return message;
}
function markForTranslationWithContext(context, message) {
return message;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 15375)
@@ -1,1847 +1,1849 @@
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;
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_MASSTRIBUTING = 10;
var inputState = INPUT_NORMAL;
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
// Distance to search for a selatable entity in. Bigger numbers are slower.
var SELECTION_SEARCH_RADIUS = 100;
// Number of pixels the mouse can move before the action is considered a drag
var maxDragDelta = 4;
// Time in milliseconds in which a double click is recognized
const doubleClickTime = 500;
var doubleClickTimer = 0;
var doubleClicked = false;
// Store the previously clicked entity - ensure a double/triple click happens on the same entity
var prevClickedEntity = 0;
// Same double-click behaviour for hotkey presses
const doublePressTime = 500;
var doublePressTimer = 0;
var prevHotkey = 0;
function updateCursorAndTooltip()
{
var cursorSet = false;
var tooltipSet = false;
var informationTooltip = Engine.GetGUIObjectByName("informationTooltip");
if (!mouseIsOverObject)
{
var action = determineAction(mouseX, mouseY);
if (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION)
{
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.SetCursor("arrow-default");
if (!tooltipSet)
informationTooltip.hidden = true;
var placementTooltip = Engine.GetGUIObjectByName("placementTooltip");
if (placementSupport.tooltipMessage)
{
if (placementSupport.tooltipError)
placementTooltip.sprite = "BackgroundErrorTooltip";
else
placementTooltip.sprite = "BackgroundInformationTooltip";
placementTooltip.caption = placementSupport.tooltipMessage;
placementTooltip.hidden = false;
}
else
{
placementTooltip.caption = "";
placementTooltip.hidden = true;
}
}
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)
{
var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed
});
// Show placement info tooltip if invalid position
placementSupport.tooltipError = !result.success;
placementSupport.tooltipMessage = "";
if (!result.success)
{
if (result.message && result.parameters)
{
var message = result.message;
if (result.translateMessage)
message = translate(message);
var parameters = result.parameters;
if (result.translateParameters)
translateObjectKeys(parameters, result.translateParameters);
placementSupport.tooltipMessage = sprintf(message, parameters);
}
return false;
}
if (placementSupport.attack)
{
// building can be placed here, and has an attack
// show the range advantage in the tooltip
var cmd = {x: placementSupport.position.x,
z: placementSupport.position.z,
range: placementSupport.attack.maxRange,
elevationBonus: placementSupport.attack.elevationBonus,
};
var averageRange = Engine.GuiInterfaceCall("GetAverageRangeForBuildings",cmd);
placementSupport.tooltipMessage = sprintf(translate("Basic range: %(range)s"), { range: Math.round(cmd.range/4) }) + "\n" + sprintf(translate("Average bonus range: %(range)s"), { range: Math.round((averageRange - cmd.range)/4) });
}
return true;
}
}
else if (placementSupport.mode === "wall")
{
if (placementSupport.wallSet && placementSupport.position)
{
// Fetch an updated list of snapping candidate entities
placementSupport.wallSnapEntities = Engine.PickSimilarFriendlyEntities(
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;
}
function findGatherType(gatherer, supply)
{
if (!("resourceGatherRates" in gatherer) || !gatherer.resourceGatherRates || !supply)
return undefined;
if (gatherer.resourceGatherRates[supply.type.generic+"."+supply.type.specific])
return supply.type.specific;
if (gatherer.resourceGatherRates[supply.type.generic])
return supply.type.generic;
return undefined;
}
function getActionInfo(action, target)
{
var simState = GetSimState();
var selection = g_Selection.toList();
// If the selection doesn't exist, no action
var entState = GetEntityState(selection[0]);
if (!entState)
return {"possible": false};
if (!target) // TODO move these non-target actions to an object like unit_actions.js
{
if (action == "set-rallypoint")
{
var cursor = "";
var data = {command: "walk"};
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data = {command: "attack-walk"};
cursor = "action-attack-move";
}
return {"possible": true, "data": data, "cursor": cursor};
}
else if (action == "move" || action == "attack-move")
return {"possible": true};
else if (action == "remove-guard")
return {"possible": true};
else
return {"possible": false};
}
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
var targetState = GetExtendedEntityState(target);
// Check if the target entity is a resource, dropsite, foundation, or enemy unit.
// Check if any entities in the selection can gather the requested resource,
// can return to the dropsite, can build the foundation, or can attack the enemy
for each (var entityID in selection)
{
var entState = GetExtendedEntityState(entityID);
if (!entState)
continue;
if (unitActions[action] && unitActions[action].getActionInfo)
{
var r = unitActions[action].getActionInfo(entState, targetState, simState);
if (r) // return true if it's possible for one of the entities
return r;
}
}
return {"possible": false};
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y, fromMinimap)
{
var selection = g_Selection.toList();
// No action if there's no selection
if (!selection.length)
{
preSelectedAction = ACTION_NONE;
return undefined;
}
// If the selection doesn't exist, no action
var entState = GetEntityState(selection[0]);
if (!entState)
return undefined;
// If the selection isn't friendly units, no action
var playerID = Engine.GetPlayerID();
var allOwnedByPlayer = selection.every(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.player == playerID;
});
if (!g_DevSettings.controlAll && !allOwnedByPlayer)
return undefined;
var targets = [];
var target = undefined;
if (!fromMinimap)
targets = Engine.PickEntitiesAtPoint(x, y, SELECTION_SEARCH_RADIUS);
if (targets.length)
target = targets[0];
// decide between the following ordered actions
// if two actions are possible, the first one is taken
// so the most specific should appear first
var actions = Object.keys(unitActions).slice();
- actions.sort(function(a, b) {return unitActions[a].specificness > unitActions[b].specificness;});
+ actions.sort(function(a, b) {return unitActions[a].specificness - unitActions[b].specificness;});
var actionInfo = undefined;
if (preSelectedAction != ACTION_NONE)
{
for (var action of actions)
{
if (unitActions[action].preSelectedActionCheck)
{
var r = unitActions[action].preSelectedActionCheck(target, selection);
if (r)
return r;
}
}
return {"type": "none", "cursor": "", "target": target};
}
for (var action of actions)
{
if (unitActions[action].hotkeyActionCheck)
{
var r = unitActions[action].hotkeyActionCheck(target, selection);
if (r)
return r;
}
}
for (var action of actions)
{
if (unitActions[action].actionCheck)
{
var r = unitActions[action].actionCheck(target, selection);
if (r)
return r;
}
}
return {"type": "none", "cursor": "", "target": target};
}
var dragStart; // used for remembering mouse coordinates at start of drag operations
function tryPlaceBuilding(queued)
{
if (placementSupport.mode !== "building")
{
error(sprintf("[%(functionName)s] Called while in '%(mode)s' placement mode instead of 'building'", {
functionName: "tryPlaceBuilding",
mode: placementSupport.mode
}));
return false;
}
// Use the preview to check it's a valid build location
if (!updateBuildingPlacementPreview())
{
// invalid location - don't build it
// TODO: play a sound?
return false;
}
var selection = g_Selection.toList();
// Start the construction
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
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
if (!queued)
placementSupport.Reset();
else
placementSupport.RandomizeActorSeed();
return true;
}
function tryPlaceWall(queued)
{
if (placementSupport.mode !== "wall")
{
error(sprintf("[%(functionName)s] Called while in '%(mode)s' placement mode; expected 'wall' mode", {
functionName: "tryPlaceWall",
mode: placementSupport.mode
}));
return false;
}
var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
{
error(sprintf("[%(functionName)s] Unexpected return value from %(function2Name)s: '%(value)s'; expected either 'false' or 'object'", {
functionName: "tryPlaceWall",
function2Name: "updateBuildingPlacementPreview",
value: uneval(placementInfo)
}));
return false;
}
if (!wallPlacementInfo)
return false;
var selection = g_Selection.toList();
var cmd = {
"type": "construct-wall",
"autorepair": true,
"autocontinue": true,
"queued": queued,
"entities": selection,
"wallSet": placementSupport.wallSet,
"pieces": wallPlacementInfo.pieces,
"startSnappedEntity": wallPlacementInfo.startSnappedEnt,
"endSnappedEntity": wallPlacementInfo.endSnappedEnt,
};
// 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)
var hasWallSegment = false;
for (var k in cmd.pieces)
{
if (cmd.pieces[k].template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
{
hasWallSegment = true;
break;
}
}
if (hasWallSegment)
{
Engine.PostNetworkCommand(cmd);
Engine.GuiInterfaceCall("PlaySound", {"name": "order_repair", "entity": selection[0] });
}
return true;
}
// Limits bandboxed selections to certain types of entities based on priority
function getPreferredEntities(ents)
{
var entStateList = [];
var preferredEnts = [];
// Check if there are units in the selection and get a list of entity states
for each (var ent in ents)
{
var entState = GetEntityState(ent);
if (!entState)
continue;
if (hasClass(entState, "Unit"))
preferredEnts.push(ent);
entStateList.push(entState);
}
// If there are no units, check if there are defensive entities in the selection
if (!preferredEnts.length)
for (var i = 0; i < ents.length; i++)
if (hasClass(entStateList[i], "Defensive"))
preferredEnts.push(ents[i]);
return preferredEnts;
}
// Removes any support units from the passed list of entities
function getMilitaryEntities(ents)
{
var militaryEnts = [];
for each (var ent in ents)
{
var entState = GetEntityState(ent);
if (!hasClass(entState, "Support"))
militaryEnts.push(ent);
}
return militaryEnts;
}
function handleInputBeforeGui(ev, hoveredObject)
{
// Capture mouse 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;
}
// Remember whether the mouse is over a GUI object or not
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))
closeMenu();
// 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:
switch (ev.type)
{
case "mousemotion":
var x0 = dragStart[0];
var y0 = dragStart[1];
var x1 = ev.x;
var y1 = ev.y;
if (x0 > x1) { var t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; }
var bandbox = Engine.GetGUIObjectByName("bandbox");
bandbox.size = [x0, y0, x1, y1].join(" ");
bandbox.hidden = false;
// TODO: Should we handle "control all units" here as well?
var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID());
g_Selection.setHighlightList(ents);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var x0 = dragStart[0];
var y0 = dragStart[1];
var x1 = ev.x;
var y1 = ev.y;
if (x0 > x1) { var t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; }
var bandbox = Engine.GetGUIObjectByName("bandbox");
bandbox.hidden = true;
// Get list of entities limited to preferred entities
// TODO: Should we handle "control all units" here as well?
var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID());
var preferredEntities = getPreferredEntities(ents)
if (preferredEntities.length)
{
ents = preferredEntities;
if (Engine.HotkeyIsPressed("selection.milonly"))
{
var militaryEntities = getMilitaryEntities(ents);
if (militaryEntities.length)
ents = militaryEntities;
}
}
// Remove the bandbox hover highlighting
g_Selection.setHighlightList([]);
// Update the list of selected units
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;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel selection
var bandbox = Engine.GetGUIObjectByName("bandbox");
bandbox.hidden = true;
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
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
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
var maxDragDelta = 16;
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BUILDING_DRAG;
return false;
}
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
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 building 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;
var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
if (result && result.cost)
{
placementSupport.tooltipMessage = getEntityCostTooltip(result);
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", result.cost);
if (neededResources)
placementSupport.tooltipMessage += getNeededResourcesTooltip(neededResources);
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
var 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;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// reset to normal input mode
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_DRAG:
switch (ev.type)
{
case "mousemotion":
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
var maxDragDelta = 16;
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
// Rotate in the direction of the mouse
var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
placementSupport.angle = Math.atan2(target.x - placementSupport.position.x, target.z - placementSupport.position.z);
}
else
{
// If the mouse is near the center, snap back to the default orientation
placementSupport.SetDefaultAngle();
}
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": 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 shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
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_MASSTRIBUTING:
if (ev.type == "hotkeyup" && ev.hotkey == "session.masstribute")
{
flushTributing();
inputState = INPUT_NORMAL;
}
break;
case INPUT_BATCHTRAINING:
if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain")
{
flushTrainingBatch();
inputState = INPUT_NORMAL;
}
break;
}
return false;
}
function handleInputAfterGui(ev)
{
if (ev.hotkey === undefined)
ev.hotkey = null;
// Handle the time-warp testing features, restricted to single-player
if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked)
{
if (ev.type == "hotkeydown" && ev.hotkey == "timewarp.fastforward")
Engine.SetSimRate(20.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.fastforward")
Engine.SetSimRate(1.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind")
Engine.RewindTimeWarp();
}
if (ev.hotkey == "session.showstatusbars")
{
g_ShowAllStatusBars = (ev.type == "hotkeydown");
recalculateStatusBarDisplay();
}
if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
if (ev.hotkey == "session.highlightguarded")
{
g_ShowGuarded = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
if (ents.length)
g_Selection.setHighlightList([ents[0]]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
dragStart = [ ev.x, ev.y ];
inputState = INPUT_SELECTING;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
return doAction(action, ev);
}
break;
case "hotkeydown":
if (ev.hotkey.indexOf("selection.group.") == 0)
{
var now = new Date();
if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey))
{
if (ev.hotkey.indexOf("selection.group.select.") == 0)
{
var sptr = ev.hotkey.split(".");
performGroup("snap", sptr[3]);
}
}
else
{
var sptr = ev.hotkey.split(".");
performGroup(sptr[2], sptr[3]);
doublePressTimer = now.getTime();
prevHotkey = ev.hotkey;
}
}
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
if (ents.length)
g_Selection.setHighlightList([ents[0]]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
return doAction(action, ev);
}
else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
// else
default:
// Slight hack: If selection is empty, reset the input state
if (g_Selection.toList().length == 0)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved further than a limit, switch to bandbox mode
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
g_Selection.setHighlightList(ents);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y, SELECTION_SEARCH_RADIUS);
if (!ents.length)
{
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.reset();
resetIdleUnit();
}
inputState = INPUT_NORMAL;
return true;
}
var selectedEntity = ents[0];
var now = new Date();
// If camera following and we select different unit, stop
if (Engine.GetFollowedEntity() != selectedEntity)
{
Engine.CameraFollow(0);
}
if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity))
{
// Double click or triple click has occurred
var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
var matchRank = true;
var templateToMatch;
// Check for double click or triple click
if (!doubleClicked)
{
// If double click hasn't already occurred, this is a double click.
// Select similar units regardless of rank
templateToMatch = GetEntityState(selectedEntity).identity.selectionGroupName;
if (templateToMatch)
{
matchRank = false;
}
else
{ // No selection group name defined, so fall back to exact match
templateToMatch = GetEntityState(selectedEntity).template;
}
doubleClicked = true;
// Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click
doubleClickTimer = now.getTime();
}
else
{
// Double click has already occurred, so this is a triple click.
// Select units matching exact template name (same rank)
templateToMatch = GetEntityState(selectedEntity).template;
}
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank, false);
}
else
{
// It's single click right now but it may become double or triple click
doubleClicked = false;
doubleClickTimer = now.getTime();
prevClickedEntity = selectedEntity;
// We only want to include the first picked unit in the selection
ents = [ents[0]];
}
// Update the list of selected units
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_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
{
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": 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")
{
var validPlacement = updateBuildingPlacementPreview();
if (validPlacement !== false)
{
inputState = INPUT_BUILDING_WALL_CLICK;
}
}
else
{
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
dragStart = [ 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":
var 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;
}
return false;
}
function doAction(action, ev)
{
var selection = g_Selection.toList();
// If shift is down, add the order to the unit's order queue instead
// of running it immediately
var queued = Engine.HotkeyIsPressed("session.queue");
var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (unitActions[action.type] && unitActions[action.type].execute)
return unitActions[action.type].execute(target, action, selection, queued);
error("Invalid action.type "+action.type);
return false;
}
function handleMinimapEvent(target)
{
// Partly duplicated from handleInputAfterGui(), but with the input being
// world coordinates instead of screen coordinates.
if (inputState != INPUT_NORMAL)
return false;
var fromMinimap = true;
var action = determineAction(undefined, undefined, fromMinimap);
if (!action)
return false;
var selection = g_Selection.toList();
var queued = Engine.HotkeyIsPressed("session.queue");
if (unitActions[action.type] && unitActions[action.type].execute)
return unitActions[action.type].execute(target, action, selection, queued);
error("Invalid action.type "+action.type);
return false;
}
// Called by GUI when user clicks construction button
// @param buildTemplate Template name of the entity the user wants to build
function startBuildingPlacement(buildTemplate, playerState)
{
- if(getEntityLimitAndCount(playerState, buildTemplate)[2] == 0)
+ if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0)
return;
// TODO: we should clear any highlight selection rings here. If the mouse 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 building.
// Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing.
placementSupport.Reset();
// find out if we're building a wall, and change the entity appropriately if so
var 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)
{
// add attack information to display a good tooltip
placementSupport.attack = templateData.attack.Ranged;
}
}
// Called by GUI when user changes required trading goods
function selectRequiredGoods(data)
{
Engine.PostNetworkCommand({"type": "select-required-goods", "entities": data.entities, "requiredGoods": data.requiredGoods});
}
// Called by GUI when user clicks exchange resources button
function exchangeResources(command)
{
Engine.PostNetworkCommand({"type": "barter", "sell": command.sell, "buy": command.buy, "amount": command.amount});
}
// Camera jumping: when the user presses a hotkey the current camera location is marked.
// When they press another hotkey the camera jumps back to that position. If the camera is already roughly at that location,
// jump back to where it was previously.
var jumpCameraPositions = [], jumpCameraLast;
function jumpCamera(index)
{
var position = jumpCameraPositions[index], distanceThreshold = Engine.ConfigDB_GetValue("user", "camerajump.threshold");
if (position)
{
if (jumpCameraLast &&
Math.abs(Engine.CameraGetX() - position.x) < distanceThreshold &&
Math.abs(Engine.CameraGetZ() - position.z) < distanceThreshold)
Engine.CameraMoveTo(jumpCameraLast.x, jumpCameraLast.z);
else
{
jumpCameraLast = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
Engine.CameraMoveTo(position.x, position.z);
}
}
}
function setJumpCamera(index)
{
jumpCameraPositions[index] = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
}
// 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 batchTrainingEntities;
var batchTrainingType;
var batchTrainingCount;
var batchTrainingEntityAllowedCount;
const batchIncrementSize = 5;
function flushTrainingBatch()
{
var appropriateBuildings = getBuildingsWhichCanTrainEntity(batchTrainingEntities, batchTrainingType);
// If training limits don't allow us to train batchTrainingCount in each appropriate building
if (batchTrainingEntityAllowedCount !== undefined &&
batchTrainingEntityAllowedCount < batchTrainingCount * appropriateBuildings.length)
{
// Train as many full batches as we can
var buildingsCountToTrainFullBatch = Math.floor(batchTrainingEntityAllowedCount / batchTrainingCount);
var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch);
Engine.PostNetworkCommand({"type": "train", "entities": buildingsToTrainFullBatch,
"template": batchTrainingType, "count": batchTrainingCount});
// Train remainer in one more building
var remainderToTrain = batchTrainingEntityAllowedCount % batchTrainingCount;
Engine.PostNetworkCommand({"type": "train",
"entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ],
"template": batchTrainingType, "count": remainderToTrain});
}
else
{
Engine.PostNetworkCommand({"type": "train", "entities": appropriateBuildings,
"template": batchTrainingType, "count": batchTrainingCount});
}
}
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(function(entity) {
var state = GetEntityState(entity);
var canTrain = state && state.production && state.production.entities.length &&
state.production.entities.indexOf(trainEntType) != -1;
return canTrain;
});
}
function getEntityLimitAndCount(playerState, entType)
{
+ var r = {
+ "entLimit": undefined,
+ "entCount": undefined,
+ "entLimitChangers": undefined,
+ "canBeAddedCount": undefined
+ };
var template = GetTemplateData(entType);
var entCategory = null;
if (template.trainingRestrictions)
entCategory = template.trainingRestrictions.category;
else if (template.buildRestrictions)
entCategory = template.buildRestrictions.category;
- var entLimit = undefined;
- var entCount = undefined;
- var entLimitChangers = undefined;
- var canBeAddedCount = undefined;
if (entCategory && playerState.entityLimits[entCategory] != null)
{
- entLimit = playerState.entityLimits[entCategory];
- entCount = playerState.entityCounts[entCategory];
- entLimitChangers = playerState.entityLimitChangers[entCategory];
- canBeAddedCount = Math.max(entLimit - entCount, 0);
+ r.entLimit = playerState.entityLimits[entCategory] || Infinity;
+ r.entCount = playerState.entityCounts[entCategory] || 0;
+ r.entLimitChangers = playerState.entityLimitChangers[entCategory];
+ r.canBeAddedCount = Math.max(r.entLimit - r.entCount, 0);
}
- return [entLimit, entCount, canBeAddedCount, entLimitChangers];
+ return r;
}
// Add the unit shown at position to the training queue for all entities in the selection
function addTrainingByPosition(position)
{
var simState = GetSimState();
var playerState = simState.players[Engine.GetPlayerID()];
var selection = g_Selection.toList();
if (!selection.length)
return;
var trainableEnts = getAllTrainableEntitiesFromSelection();
// Check if the position is valid
if (!trainableEnts.length || trainableEnts.length <= position)
return;
var entToTrain = trainableEnts[position];
addTrainingToQueue(selection, entToTrain, playerState);
return;
}
// Called by GUI when user clicks training button
function addTrainingToQueue(selection, trainEntType, playerState)
{
// Create list of buildings which can train trainEntType
var appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
// Check trainEntType entity limit and count
- var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType)
+ var limits = getEntityLimitAndCount(playerState, trainEntType);
// Batch training possible if we can train at least 2 units
- var batchTrainingPossible = canBeTrainedCount == undefined || canBeTrainedCount > 1;
+ var batchTrainingPossible = limits.canBeAddedCount == undefined || limits.canBeAddedCount > 1;
var decrement = Engine.HotkeyIsPressed("selection.remove");
if (!decrement)
var template = GetTemplateData(trainEntType);
if (Engine.HotkeyIsPressed("session.batchtrain") && batchTrainingPossible)
{
if (inputState == INPUT_BATCHTRAINING)
{
// Check if we are training in the same building(s) as the last batch
var sameEnts = false;
if (batchTrainingEntities.length == selection.length)
{
// 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.
for (var i = 0; i < batchTrainingEntities.length; ++i)
{
if (!(sameEnts = batchTrainingEntities[i] == selection[i]))
break;
}
}
// If we're already creating a batch of this unit (in the same building(s)), then just extend it
// (if training limits allow)
if (sameEnts && batchTrainingType == trainEntType)
{
if (decrement)
{
batchTrainingCount -= batchIncrementSize;
if (batchTrainingCount <= 0)
inputState = INPUT_NORMAL;
}
- else if (canBeTrainedCount == undefined ||
- canBeTrainedCount > batchTrainingCount * appropriateBuildings.length)
+ else if (limits.canBeAddedCount == undefined ||
+ limits.canBeAddedCount > batchTrainingCount * appropriateBuildings.length)
{
if (Engine.GuiInterfaceCall("GetNeededResources", multiplyEntityCosts(
template, batchTrainingCount + batchIncrementSize)))
return;
batchTrainingCount += batchIncrementSize;
}
- batchTrainingEntityAllowedCount = canBeTrainedCount;
+ batchTrainingEntityAllowedCount = limits.canBeAddedCount;
return;
}
// Otherwise start a new one
else if (!decrement)
{
flushTrainingBatch();
// fall through to create the new batch
}
}
// Don't start a new batch if decrementing or unable to afford it.
if (decrement || Engine.GuiInterfaceCall("GetNeededResources",
multiplyEntityCosts(template, batchIncrementSize)))
return;
inputState = INPUT_BATCHTRAINING;
batchTrainingEntities = selection;
batchTrainingType = trainEntType;
- batchTrainingEntityAllowedCount = canBeTrainedCount;
+ batchTrainingEntityAllowedCount = limits.canBeAddedCount;
batchTrainingCount = batchIncrementSize;
}
else
{
// Non-batched - just create a single entity in each building
// (but no more than entity limit allows)
var buildingsForTraining = appropriateBuildings;
- if (trainEntLimit)
- buildingsForTraining = buildingsForTraining.slice(0, canBeTrainedCount);
+ if (limits.entLimit)
+ buildingsForTraining = buildingsForTraining.slice(0, limits.canBeAddedCount);
Engine.PostNetworkCommand({"type": "train", "template": trainEntType,
"count": 1, "entities": buildingsForTraining});
}
}
// Called by GUI when user clicks research button
function addResearchToQueue(entity, researchType)
{
Engine.PostNetworkCommand({"type": "research", "entity": entity, "template": researchType});
}
// Returns the number of units that will be present in a batch if the user clicks
// the training button with shift down
function getTrainingBatchStatus(playerState, entity, trainEntType, selection)
{
var appropriateBuildings = [entity];
if (selection && selection.indexOf(entity) != -1)
appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
var nextBatchTrainingCount = 0;
var currentBatchTrainingCount = 0;
if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 &&
batchTrainingType == trainEntType)
{
nextBatchTrainingCount = batchTrainingCount;
currentBatchTrainingCount = batchTrainingCount;
- var canBeTrainedCount = batchTrainingEntityAllowedCount;
+ var limits = {
+ "canBeAddedCount": batchTrainingEntityAllowedCount
+ };
}
else
{
- var [trainEntLimit, trainEntCount, canBeTrainedCount] =
- getEntityLimitAndCount(playerState, trainEntType);
- var batchSize = Math.min(canBeTrainedCount, batchIncrementSize);
+ var limits = getEntityLimitAndCount(playerState, trainEntType);
}
// We need to calculate count after the next increment if it's possible
- if (canBeTrainedCount == undefined ||
- canBeTrainedCount > nextBatchTrainingCount * appropriateBuildings.length)
+ if (limits.canBeAddedCount == undefined ||
+ limits.canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length)
nextBatchTrainingCount += batchIncrementSize;
// If training limits don't allow us to train batchTrainingCount in each appropriate building
// train as many full batches as we can and remainer in one more building.
var buildingsCountToTrainFullBatch = appropriateBuildings.length;
var remainderToTrain = 0;
- if (canBeTrainedCount !== undefined &&
- canBeTrainedCount < nextBatchTrainingCount * appropriateBuildings.length)
+ if (limits.canBeAddedCount !== undefined &&
+ limits.canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length)
{
- buildingsCountToTrainFullBatch = Math.floor(canBeTrainedCount / nextBatchTrainingCount);
- remainderToTrain = canBeTrainedCount % nextBatchTrainingCount;
+ buildingsCountToTrainFullBatch = Math.floor(limits.canBeAddedCount / nextBatchTrainingCount);
+ remainderToTrain = limits.canBeAddedCount % nextBatchTrainingCount;
}
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain, currentBatchTrainingCount];
}
// Called by GUI when user clicks production queue item
function removeFromProductionQueue(entity, id)
{
Engine.PostNetworkCommand({"type": "stop-production", "entity": entity, "id": id});
}
// Called by unit selection buttons
function changePrimarySelectionGroup(templateName, deselectGroup)
{
if (Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup)
g_Selection.makePrimarySelection(templateName, true);
else
g_Selection.makePrimarySelection(templateName, false);
}
// Performs the specified command (delete, town bell, repair, etc.)
function performCommand(entity, commandName)
{
if (!entity)
return;
var entState = GetExtendedEntityState(entity);
var playerID = Engine.GetPlayerID();
if (!entState.player == playerID && !g_DevSettings.controlAll)
return;
if (entityCommands[commandName])
entityCommands[commandName].execute(entState);
}
// Performs the specified formation
function performFormation(entity, formationTemplate)
{
if (entity)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "formation",
"entities": selection,
"name": formationTemplate
});
}
}
// Performs the specified group
function performGroup(action, groupId)
{
switch (action)
{
case "snap":
case "select":
case "add":
var toSelect = [];
g_Groups.update();
for (var 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)
Engine.CameraFollow(toSelect[0]);
break;
case "save":
case "breakUp":
g_Groups.groups[groupId].reset();
if (action == "save")
g_Groups.addEntities(groupId, g_Selection.toList());
updateGroups();
break;
}
}
// Performs the specified stance
function performStance(entity, stanceName)
{
if (entity)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "stance",
"entities": selection,
"name": stanceName
});
}
}
// Lock / Unlock the gate
function lockGate(lock)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "lock-gate",
"entities": selection,
"lock": lock,
});
}
// Pack / unpack unit(s)
function packUnit(pack)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "pack",
"entities": selection,
"pack": pack,
"queued": false
});
}
// Cancel un/packing unit(s)
function cancelPackUnit(pack)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "cancel-pack",
"entities": selection,
"pack": pack,
"queued": false
});
}
// Transform a wall to a gate
function transformWallToGate(template)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "wall-to-gate",
"entities": selection.filter( function(e) { return getWallGateTemplate(e) == template } ),
"template": template,
});
}
// Gets the gate form (if any) of a given long wall piece
function getWallGateTemplate(entity)
{
// TODO: find the gate template name in a better way
var entState = GetEntityState(entity);
var index;
if (entState && !entState.foundation && hasClass(entState, "LongWall") && (index = entState.template.indexOf("long")) >= 0)
return entState.template.substr(0, index) + "gate";
return undefined;
}
// Set the camera to follow the given unit
function setCameraFollow(entity)
{
// Follow the given entity if it's a unit
if (entity)
{
var entState = GetEntityState(entity);
if (entState && hasClass(entState, "Unit"))
{
Engine.CameraFollow(entity);
return;
}
}
// Otherwise stop following
Engine.CameraFollow(0);
}
var lastIdleUnit = 0;
var currIdleClass = 0;
var lastIdleType = undefined;
function resetIdleUnit()
{
lastIdleUnit = 0;
currIdleClass = 0;
lastIdleType = undefined;
}
function findIdleUnit(classes)
{
var append = Engine.HotkeyIsPressed("selection.add");
var selectall = Engine.HotkeyIsPressed("selection.offscreen");
// Reset the last idle unit, etc., if the selection type has changed.
var type = classes.join();
if (selectall || type != lastIdleType)
resetIdleUnit();
lastIdleType = type;
// If selectall is true, there is no limit and it's necessary to iterate
// over all of the classes, resetting only when the first match is found.
var matched = false;
for (var i = 0; i < classes.length; ++i)
{
var data = {
"idleClass": classes[currIdleClass],
"prevUnit": lastIdleUnit,
"limit": 1,
"excludeUnits": []
};
if (append)
data.excludeUnits = g_Selection.toList();
if (selectall)
data = { idleClass: classes[currIdleClass] };
// Check if we have new valid entity
var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
if (idleUnits.length && idleUnits[0] != lastIdleUnit)
{
lastIdleUnit = idleUnits[0];
if (!append && (!selectall || selectall && !matched))
g_Selection.reset()
if (selectall)
g_Selection.addList(idleUnits);
else
{
g_Selection.addList([lastIdleUnit]);
var position = GetEntityState(lastIdleUnit).position;
if (position)
Engine.CameraMoveTo(position.x, position.z);
return;
}
matched = true;
}
lastIdleUnit = 0;
currIdleClass = (currIdleClass + 1) % classes.length;
}
// TODO: display a message or play a sound to indicate no more idle units, or something
// Reset for next cycle
resetIdleUnit();
}
function stopUnits(entities)
{
Engine.PostNetworkCommand({ "type": "stop", "entities": entities, "queued": false });
}
function unload(garrisonHolder, entities)
{
if (Engine.HotkeyIsPressed("session.unloadtype"))
Engine.PostNetworkCommand({"type": "unload", "entities": entities, "garrisonHolder": garrisonHolder});
else
Engine.PostNetworkCommand({"type": "unload", "entities": [entities[0]], "garrisonHolder": garrisonHolder});
}
function unloadTemplate(template)
{
// Filter out all entities that aren't garrisonable.
var garrisonHolders = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
if (state && state.garrisonHolder)
return true;
return false;
});
Engine.PostNetworkCommand({
"type": "unload-template",
"all": Engine.HotkeyIsPressed("session.unloadtype"),
"template": template,
"garrisonHolders": garrisonHolders
});
}
function unloadSelection()
{
var entities = g_Selection.toList();
var parent = 0;
var ents = [];
for each (var ent in entities)
{
var state = GetExtendedEntityState(ent);
if (!state || !state.turretParent)
continue;
if (!parent)
{
parent = state.turretParent;
ents.push(ent);
}
else if (state.turretParent == parent)
ents.push(ent)
}
if (parent)
Engine.PostNetworkCommand({"type": "unload", "entities":ents, "garrisonHolder": parent});
}
function unloadAll()
{
// Filter out all entities that aren't garrisonable.
var garrisonHolders = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
if (state && state.garrisonHolder)
return true;
return false;
});
Engine.PostNetworkCommand({"type": "unload-all", "garrisonHolders": garrisonHolders});
}
function backToWork()
{
// Filter out all entities that can't go back to work.
var workers = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.unitAI && state.unitAI.hasWorkOrders);
});
Engine.PostNetworkCommand({"type": "back-to-work", "entities": workers});
}
function removeGuard()
{
// Filter out all entities that are currently guarding/escorting.
var entities = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.unitAI && state.unitAI.isGuarding);
});
Engine.PostNetworkCommand({"type": "remove-guard", "entities": entities});
}
function increaseAlertLevel()
{
var entities = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.alertRaiser && state.alertRaiser.canIncreaseLevel);
});
Engine.PostNetworkCommand({"type": "increase-alert-level", "entities": entities});
}
function endOfAlert()
{
var entities = g_Selection.toList().filter(function(e) {
var state = GetEntityState(e);
return (state && state.alertRaiser && state.alertRaiser.hasRaisedAlert);
});
Engine.PostNetworkCommand({"type": "alert-end", "entities": entities});
}
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.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 15375)
@@ -1,512 +1,530 @@
// Limits selection size
const MAX_SELECTION_SIZE = 200;
// Alpha value of hovered/mouseover/highlighted selection overlays
// (should probably be greater than always visible alpha value,
// see CCmpSelectable)
const HIGHLIGHTED_ALPHA = 0.75;
function _setHighlight(ents, alpha, selected)
{
if (ents.length)
Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities":ents, "alpha":alpha, "selected":selected });
}
function _setStatusBars(ents, enabled)
{
if (ents.length)
Engine.GuiInterfaceCall("SetStatusBars", { "entities":ents, "enabled":enabled });
}
function _setMotionOverlay(ents, enabled)
{
if (ents.length)
Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities":ents, "enabled":enabled });
}
function _playSound(ent)
{
Engine.GuiInterfaceCall("PlaySound", { "name":"select", "entity":ent });
}
/**
* EntityGroups class for managing grouped entities
*/
function EntityGroups()
{
this.groups = {};
this.ents = {};
}
EntityGroups.prototype.reset = function()
{
this.groups = {};
this.ents = {};
};
EntityGroups.prototype.add = function(ents)
{
for each (var ent in ents)
{
if (!this.ents[ent])
{
var entState = GetEntityState(ent);
// When this function is called during group rebuild, deleted
// entities will not yet have been removed, so entities might
// still be present in the group despite not existing.
if (!entState)
continue;
var templateName = entState.template;
var key = GetTemplateData(templateName).selectionGroupName || templateName;
+ // TODO ugly hack, just group them by player too.
// Prefix garrisoned unit's selection name with the player they belong to
var index = templateName.indexOf("&");
if (index != -1 && key.indexOf("&") == -1)
key = templateName.slice(0, index+1) + key;
if (this.groups[key])
this.groups[key] += 1;
else
this.groups[key] = 1;
this.ents[ent] = key;
}
}
};
EntityGroups.prototype.removeEnt = function(ent)
{
var templateName = this.ents[ent];
// Remove the entity
delete this.ents[ent];
this.groups[templateName]--;
// Remove the entire group
if (this.groups[templateName] == 0)
delete this.groups[templateName];
};
EntityGroups.prototype.rebuildGroup = function(renamed)
{
var oldGroup = this.ents;
this.reset();
var toAdd = [];
for (var ent in oldGroup)
toAdd.push(renamed[ent] ? renamed[ent] : +ent);
this.add(toAdd);
}
EntityGroups.prototype.getCount = function(templateName)
{
return this.groups[templateName];
};
EntityGroups.prototype.getTotalCount = function()
{
var totalCount = 0;
for each (var group in this.groups)
{
totalCount += group;
}
return totalCount;
};
EntityGroups.prototype.getTemplateNames = function()
{
var templateNames = [];
for (var templateName in this.groups)
templateNames.push(templateName);
//Preserve order even when shuffling units around
//Can be optimized by moving the sorting elsewhere
templateNames.sort();
return templateNames;
};
EntityGroups.prototype.getEntsByName = function(templateName)
{
var ents = [];
for (var ent in this.ents)
{
if (this.ents[ent] == templateName)
ents.push(+ent);
}
return ents;
};
/**
+ * get a list of entities grouped by templateName
+ */
+EntityGroups.prototype.getEntsGrouped = function()
+{
+ var templateNames = this.getTemplateNames();
+ var list = [];
+ for (var t of templateNames)
+ {
+ list.push({
+ "ents": this.getEntsByName(t),
+ "template": t,
+ });
+ }
+ return list;
+};
+
+/**
* Gets all ents in every group except ones of the specified group
*/
EntityGroups.prototype.getEntsByNameInverse = function(templateName)
{
var ents = [];
for (var ent in this.ents)
{
if (this.ents[ent] != templateName)
ents.push(+ent);
}
return ents;
};
/**
* EntitySelection class for managing the entity selection list and the primary selection
*/
function EntitySelection()
{
// Private properties:
//--------------------------------
this.selected = {}; // { id:id, id:id, ... } for each selected entity ID 'id'
// { id:id, ... } for mouseover-highlighted entity IDs in these, the key is a string and the value is an int;
// we want to use the int form wherever possible since it's more efficient to send to the simulation code)
this.highlighted = {};
this.motionDebugOverlay = false;
// Public properties:
//--------------------------------
this.dirty = false; // set whenever the selection has changed
this.groups = new EntityGroups();
}
/**
* Deselect everything but entities of the chosen type if the modifier is true otherwise deselect just the chosen entity
*/
EntitySelection.prototype.makePrimarySelection = function(templateName, modifierKey)
{
var selection = this.toList();
var template = GetTemplateData(templateName);
var key = template.selectionGroupName || templateName;
var ents = [];
if (modifierKey)
ents = this.groups.getEntsByNameInverse(key);
else
ents = this.groups.getEntsByName(key);
this.reset();
this.addList(ents);
}
/**
* Get a list of the template names
*/
EntitySelection.prototype.getTemplateNames = function()
{
var templateNames = [];
var ents = this.toList();
for each (var ent in ents)
{
var entState = GetEntityState(ent);
if (entState)
templateNames.push(entState.template);
}
return templateNames;
}
/**
* Update the selection to take care of changes (like units that have been killed)
*/
EntitySelection.prototype.update = function()
{
var changed = false;
this.checkRenamedEntities();
for each (var ent in this.selected)
{
var entState = GetEntityState(ent);
// Remove deleted units
if (!entState)
{
delete this.selected[ent];
this.groups.removeEnt(ent);
changed = true;
continue;
}
// Remove non-visible units (e.g. moved back into fog-of-war)
if (entState.visibility == "hidden")
{
// Disable any highlighting of the disappeared unit
_setHighlight([ent], 0, false);
_setStatusBars([ent], false);
_setMotionOverlay([ent], false);
delete this.selected[ent];
this.groups.removeEnt(ent);
changed = true;
continue;
}
}
if (changed)
this.onChange();
};
/**
* Update selection if some selected entities were renamed
* (in case of unit promotion or finishing building structure)
*/
EntitySelection.prototype.checkRenamedEntities = function()
{
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
if (renamedEntities.length > 0)
{
var renamedLookup = {};
for each (var renamedEntity in renamedEntities)
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
// Reconstruct the selection if at least one entity has been renamed.
for each (var renamedEntity in renamedEntities)
{
if (this.selected[renamedEntity.entity])
{
this.rebuildSelection(renamedLookup);
break;
}
}
}
}
/**
* Add entities to selection. Play selection sound unless quiet is true
*/
EntitySelection.prototype.addList = function(ents, quiet)
{
var selection = this.toList();
var playerID = Engine.GetPlayerID();
// If someone else's player is the sole selected unit, don't allow adding to the selection
if (!g_DevSettings.controlAll && selection.length == 1)
{
var firstEntState = GetEntityState(selection[0]);
if (firstEntState && firstEntState.player != playerID)
return;
}
// Allow selecting things not belong to this player (enemy, ally, gaia)
var allowUnownedSelect = g_DevSettings.controlAll || (ents.length == 1 && selection.length == 0);
var i = 1;
var added = [];
for each (var ent in ents)
{
// Only add entities we own to our selection
var entState = GetEntityState(ent);
if (!this.selected[ent] && (selection.length + i) <= MAX_SELECTION_SIZE && (allowUnownedSelect || (entState && entState.player == playerID)))
{
added.push(ent);
this.selected[ent] = ent;
i++;
}
}
_setHighlight(added, 1, true);
_setStatusBars(added, true);
_setMotionOverlay(added, this.motionDebugOverlay);
if (added.length)
{
// Play the sound if the entity is controllable by us or Gaia-owned.
var owner = GetEntityState(added[0]).player;
if (!quiet && (owner == playerID || owner == 0 || g_DevSettings.controlAll))
_playSound(added[0]);
}
this.groups.add(this.toList()); // Create Selection Groups
this.onChange();
};
EntitySelection.prototype.removeList = function(ents)
{
var removed = [];
for each (var ent in ents)
{
if (this.selected[ent])
{
this.groups.removeEnt(ent);
removed.push(ent);
delete this.selected[ent];
}
}
_setHighlight(removed, 0, false);
_setStatusBars(removed, false);
_setMotionOverlay(removed, false);
this.onChange();
};
EntitySelection.prototype.reset = function()
{
_setHighlight(this.toList(), 0, false);
_setStatusBars(this.toList(), false);
_setMotionOverlay(this.toList(), false);
this.selected = {};
this.groups.reset();
this.onChange();
};
EntitySelection.prototype.rebuildSelection = function(renamed)
{
var oldSelection = this.selected;
this.reset();
var toAdd = [];
for each (var ent in oldSelection)
toAdd.push(renamed[ent] ? renamed[ent] : ent);
this.addList(toAdd, true); // don't play selection sounds
}
EntitySelection.prototype.toList = function(){
var ents = [];
for each (var ent in this.selected)
ents.push(ent);
return ents;
};
EntitySelection.prototype.setHighlightList = function(ents)
{
var highlighted = {};
for each (var ent in ents)
highlighted[ent] = ent;
var removed = [];
var added = [];
// Remove highlighting for the old units that are no longer highlighted
// (excluding ones that are actively selected too)
for each (var ent in this.highlighted)
if (!highlighted[ent] && !this.selected[ent])
removed.push(+ent);
// Add new highlighting for units that aren't already highlighted
for each (var ent in ents)
if (!this.highlighted[ent] && !this.selected[ent])
added.push(+ent);
_setHighlight(removed, 0, false);
_setStatusBars(removed, false);
_setHighlight(added, HIGHLIGHTED_ALPHA, true);
_setStatusBars(added, true);
// Store the new highlight list
this.highlighted = highlighted;
};
EntitySelection.prototype.SetMotionDebugOverlay = function(enabled)
{
this.motionDebugOverlay = enabled;
_setMotionOverlay(this.toList(), enabled);
};
EntitySelection.prototype.onChange = function()
{
this.dirty = true;
if (this == g_Selection)
onSelectionChange();
}
/**
* Cache some quantities which depends only on selection
*/
var g_Selection = new EntitySelection();
var g_canMoveIntoFormation = {};
var g_allBuildableEntities = undefined;
var g_allTrainableEntities = undefined;
// Reset cached quantities
function onSelectionChange()
{
g_canMoveIntoFormation = {};
g_allBuildableEntities = undefined;
g_allTrainableEntities = undefined;
}
/**
* EntityGroupsContainer class for managing grouped entities
*/
function EntityGroupsContainer()
{
this.groups = [];
for (var i = 0; i < 10; ++i)
{
this.groups[i] = new EntityGroups();
}
}
EntityGroupsContainer.prototype.addEntities = function(groupName, ents)
{
for each (var ent in ents)
{
for each (var group in this.groups)
{
if (ent in group.ents)
{
group.removeEnt(ent);
}
}
}
this.groups[groupName].add(ents);
}
EntityGroupsContainer.prototype.update = function()
{
this.checkRenamedEntities();
for each (var group in this.groups)
{
for (var ent in group.ents)
{
var entState = GetEntityState(+ent);
// Remove deleted units
if (!entState)
{
group.removeEnt(ent);
}
}
}
}
/**
* Update control group if some entities in the group were renamed
* (in case of unit promotion or finishing building structure)
*/
EntityGroupsContainer.prototype.checkRenamedEntities = function()
{
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
if (renamedEntities.length > 0)
{
var renamedLookup = {};
for each (var renamedEntity in renamedEntities)
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
for each (var group in this.groups)
{
for each (var renamedEntity in renamedEntities)
{
// Reconstruct the group if at least one entity has been renamed.
if (renamedEntity.entity in group.ents)
{
group.rebuildGroup(renamedLookup);
break;
}
}
}
}
}
var g_Groups = new EntityGroupsContainer();
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 15375)
@@ -1,435 +1,432 @@
function layoutSelectionSingle()
{
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
function layoutSelectionMultiple()
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
function getResourceTypeDisplayName(resourceType)
{
var resourceCode = resourceType["generic"];
var displayName = "";
if (resourceCode == "treasure")
displayName = getLocalizedResourceName(resourceType["specific"], "firstWord");
else
displayName = getLocalizedResourceName(resourceCode, "firstWord");
return displayName;
}
// Fills out information that most entities have
function displaySingle(entState, template)
{
// Get general unit and player data
var specificName = template.name.specific;
var genericName = template.name.generic != template.name.specific ? template.name.generic : "";
// If packed, add that to the generic name (reduces template clutter)
if (genericName && template.pack && template.pack.state == "packed")
genericName = sprintf(translate("%(genericName)s — Packed"), { genericName: genericName });
var playerState = g_Players[entState.player];
var civName = g_CivData[playerState.civ].Name;
var civEmblem = g_CivData[playerState.civ].Emblem;
var playerName = playerState.name;
var playerColor = playerState.color.r + " " + playerState.color.g + " " + playerState.color.b + " 128";
// Indicate disconnected players by prefixing their name
if (g_Players[entState.player].offline)
{
playerName = sprintf(translate("[OFFLINE] %(player)s"), { player: playerName });
}
// Rank
if (entState.identity && entState.identity.rank && entState.identity.classes)
{
Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), { rank: translateWithContext("Rank", entState.identity.rank) });
Engine.GetGUIObjectByName("rankIcon").sprite = getRankIconSprite(entState);
Engine.GetGUIObjectByName("rankIcon").hidden = false;
}
else
{
Engine.GetGUIObjectByName("rankIcon").hidden = true;
Engine.GetGUIObjectByName("rankIcon").tooltip = "";
}
// Hitpoints
if (entState.hitpoints)
{
var unitHealthBar = Engine.GetGUIObjectByName("healthBar");
var healthSize = unitHealthBar.size;
healthSize.rright = 100*Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints));
unitHealthBar.size = healthSize;
Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), {
hitpoints: Math.ceil(entState.hitpoints),
maxHitpoints: entState.maxHitpoints
});
Engine.GetGUIObjectByName("healthSection").hidden = false;
}
else
{
Engine.GetGUIObjectByName("healthSection").hidden = true;
}
// TODO: Stamina
var player = Engine.GetPlayerID();
if (entState.stamina && (entState.player == player || g_DevSettings.controlAll))
Engine.GetGUIObjectByName("staminaSection").hidden = false;
else
Engine.GetGUIObjectByName("staminaSection").hidden = true;
// Experience
if (entState.promotion)
{
var experienceBar = Engine.GetGUIObjectByName("experienceBar");
var experienceSize = experienceBar.size;
experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req)));
experienceBar.size = experienceSize;
if (entState.promotion.curr < entState.promotion.req)
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), {
experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
current: Math.floor(entState.promotion.curr),
required: entState.promotion.req
});
else
Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), {
experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
current: Math.floor(entState.promotion.curr)
});
Engine.GetGUIObjectByName("experience").hidden = false;
}
else
{
Engine.GetGUIObjectByName("experience").hidden = true;
}
// Resource stats
if (entState.resourceSupply)
{
var resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol
sprintf(translate("%(amount)s / %(max)s"), { amount: Math.ceil(+entState.resourceSupply.amount), max: entState.resourceSupply.max });
var resourceType = getResourceTypeDisplayName(entState.resourceSupply.type);
var unitResourceBar = Engine.GetGUIObjectByName("resourceBar");
var resourceSize = unitResourceBar.size;
resourceSize.rright = entState.resourceSupply.isInfinite ? 100 :
100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max));
unitResourceBar.size = resourceSize;
Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), { resource: resourceType });
Engine.GetGUIObjectByName("resourceStats").caption = resources;
if (entState.hitpoints)
Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("staminaSection").size;
else
Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("healthSection").size;
Engine.GetGUIObjectByName("resourceSection").hidden = false;
}
else
{
Engine.GetGUIObjectByName("resourceSection").hidden = true;
}
// Resource carrying
if (entState.resourceCarrying && entState.resourceCarrying.length)
{
// We should only be carrying one resource type at once, so just display the first
var carried = entState.resourceCarrying[0];
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+carried.type+".png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: carried.amount, max: carried.max });
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = "";
}
// Use the same indicators for traders
else if (entState.trader && entState.trader.goods.amount)
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+entState.trader.goods.type+".png";
var totalGain = entState.trader.goods.amount.traderGain;
if (entState.trader.goods.amount.market1Gain)
totalGain += entState.trader.goods.amount.market1Gain;
if (entState.trader.goods.amount.market2Gain)
totalGain += entState.trader.goods.amount.market2Gain;
Engine.GetGUIObjectByName("resourceCarryingText").caption = totalGain;
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Gain: %(amount)s"), { amount: getTradingTooltip(entState.trader.goods.amount) });
}
// And for number of workers
else if (entState.foundation)
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " ";
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Number of builders");
}
else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints))
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = false;
Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png";
Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: entState.resourceSupply.gatherers.length, max: entState.resourceSupply.maxGatherers }) + " ";
Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers");
}
else
{
Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = true;
Engine.GetGUIObjectByName("resourceCarryingText").hidden = true;
}
// Set Player details
Engine.GetGUIObjectByName("specific").caption = specificName;
Engine.GetGUIObjectByName("player").caption = playerName;
Engine.GetGUIObjectByName("playerColorBackground").sprite = "colour: " + playerColor;
if (genericName)
{
Engine.GetGUIObjectByName("generic").caption = sprintf(translate("(%(genericName)s)"), { genericName: genericName });
}
else
{
Engine.GetGUIObjectByName("generic").caption = "";
}
if ("gaia" != playerState.civ)
{
Engine.GetGUIObjectByName("playerCivIcon").sprite = "stretched:grayscale:" + civEmblem;
Engine.GetGUIObjectByName("player").tooltip = civName;
}
else
{
Engine.GetGUIObjectByName("playerCivIcon").sprite = "";
Engine.GetGUIObjectByName("player").tooltip = "";
}
// Icon image
if (template.icon)
{
Engine.GetGUIObjectByName("icon").sprite = "stretched:session/portraits/" + template.icon;
}
else
{
// TODO: we should require all entities to have icons, so this case never occurs
Engine.GetGUIObjectByName("icon").sprite = "bkFillBlack";
}
var armorLabel = "[font=\"sans-bold-13\"]" + translate("Armor:") + "[/font]"
var armorString = sprintf(translate("%(label)s %(details)s"), { label: armorLabel, details: armorTypeDetails(entState.armour) });
// Attack and Armor
if ("attack" in entState && entState.attack)
{
// Rate
if (entState.buildingAI)
var rateLabel = "[font=\"sans-bold-13\"]" + translate("Interval:") + "[/font]";
else
var rateLabel = "[font=\"sans-bold-13\"]" + translate("Rate:") + "[/font]";
var rate = sprintf(translate("%(label)s %(details)s"), {
label: rateLabel,
details: attackRateDetails(entState)
});
var attack;
var label = "[font=\"sans-bold-13\"]" + getAttackTypeLabel(entState.attack.type) + "[/font]"
if (entState.attack.type == "Ranged")
{
var realRange = entState.attack.elevationAdaptedRange;
var range = entState.attack.maxRange;
var rangeLabel = "[font=\"sans-bold-13\"]" + translate("Range:") + "[/font]"
var relativeRange = Math.round((realRange - range));
var meters = "[font=\"sans-10\"][color=\"orange\"]" + translate("meters") + "[/color][/font]";
if (relativeRange > 0)
attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rangeLabel: rangeLabel,
range: Math.round(range),
meters: meters,
relative: "+" + relativeRange,
rate: rate
});
else if (relativeRange < 0)
attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rangeLabel: rangeLabel,
range: Math.round(range),
meters: meters,
relative: relativeRange,
rate: rate
});
else // don't show when it's 0
attack = sprintf(translate("%(label)s %(details)s, %(rangeLabel)s %(range)s %(meters)s, %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rangeLabel: rangeLabel,
range: Math.round(range),
meters: meters,
rate: rate
});
}
else
{
attack = sprintf(translate("%(label)s %(details)s, %(rate)s"), {
label: label,
details: damageTypeDetails(entState.attack),
rate: rate
});
}
Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = attack + "\n" + armorString;
}
else
{
Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = armorString;
}
// Icon Tooltip
var iconTooltip = "";
if (genericName)
iconTooltip = "[font=\"sans-bold-16\"]" + genericName + "[/font]";
if (template.visibleIdentityClasses && template.visibleIdentityClasses.length)
{
iconTooltip += "\n[font=\"sans-bold-13\"]" + translate("Classes:") + "[/font] ";
iconTooltip += "[font=\"sans-13\"]" + translate(template.visibleIdentityClasses[0]) ;
for (var i = 1; i < template.visibleIdentityClasses.length; i++)
iconTooltip += ", " + translate(template.visibleIdentityClasses[i]);
iconTooltip += "[/font]";
}
if (template.auras)
{
for (var auraName in template.auras)
{
iconTooltip += "\n[font=\"sans-bold-13\"]" + translate(auraName) + "[/font]";
if (template.auras[auraName])
iconTooltip += ": " + translate(template.auras[auraName]);
}
}
if (template.tooltip)
iconTooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";
Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltip;
// Unhide Details Area
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false;
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
}
// Fills out information for multiple entities
function displayMultiple(selection, template)
{
var averageHealth = 0;
var maxHealth = 0;
for (var i = 0; i < selection.length; i++)
{
var entState = GetEntityState(selection[i])
if (entState)
{
if (entState.hitpoints)
{
averageHealth += entState.hitpoints;
maxHealth += entState.maxHitpoints;
}
}
}
if (averageHealth > 0)
{
var unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple");
var healthSize = unitHealthBar.size;
healthSize.rtop = 100-100*Math.max(0, Math.min(1, averageHealth / maxHealth));
unitHealthBar.size = healthSize;
var hitpointsLabel = "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]"
var hitpoints = sprintf(translate("%(label)s %(current)s / %(max)s"), { label: hitpointsLabel, current: averageHealth, max: maxHealth });
var healthMultiple = Engine.GetGUIObjectByName("healthMultiple");
healthMultiple.tooltip = hitpoints;
healthMultiple.hidden = false;
}
else
{
Engine.GetGUIObjectByName("healthMultiple").hidden = true;
}
// TODO: Stamina
// Engine.GetGUIObjectByName("staminaBarMultiple");
Engine.GetGUIObjectByName("numberOfUnits").caption = selection.length;
// Unhide Details Area
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
// Updates middle entity Selection Details Panel
function updateSelectionDetails()
{
var supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails");
var detailsPanel = Engine.GetGUIObjectByName("selectionDetails");
var commandsPanel = Engine.GetGUIObjectByName("unitCommands");
g_Selection.update();
var selection = g_Selection.toList();
if (selection.length == 0)
{
Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true;
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
hideUnitCommands();
supplementalDetailsPanel.hidden = true;
detailsPanel.hidden = true;
commandsPanel.hidden = true;
return;
}
/* If the unit has no data (e.g. it was killed), don't try displaying any
data for it. (TODO: it should probably be removed from the selection too;
also need to handle multi-unit selections) */
var entState = GetExtendedEntityState(selection[0]);
if (!entState)
return;
var template = GetTemplateData(entState.template);
// Fill out general info and display it
if (selection.length == 1)
displaySingle(entState, template);
else
displayMultiple(selection, template);
// Show basic details.
detailsPanel.hidden = false;
if (g_IsObserver)
{
// Observers don't need these displayed.
supplementalDetailsPanel.hidden = true;
commandsPanel.hidden = true;
}
else
{
// Fill out commands panel for specific unit selected (or first unit of primary group)
updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection);
- // Show panels
- supplementalDetailsPanel.hidden = false;
- commandsPanel.hidden = false;
}
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 15375)
@@ -1,610 +1,935 @@
/**
* Contains the layout and button settings per selection panel
*
- * addData is called first, and can be used to abort going any furter
- * by returning false. Else it should always return true.
+ * 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.
*
- * addData is used to add data to the data object that can be used in the
- * content setter functions.
+ * The standard data is
+ * var data = {
+ * "i": index
+ * "item": item coming from the getItems function
+ * "selection": list of currently selected items
+ * "playerState": playerState
+ * "unitEntState": first selected entity state
+ * "rowLength": rowLength
+ * "numberOfItems": number of items that will be processed
+ * "button": gui Button object
+ * "affordableMask": gui Unaffordable overlay
+ * "icon": gui Icon object
+ * "guiSelection": gui button Selection overlay
+ * "countDisplay": gui caption space
+ * };
+ *
+ * Then, addData is called, and can be used to abort the processing
+ * of the current item by returning false.
+ * It should return true if you want the panel to be filled.
+ *
+ * addData is used to add data to the data object on top
+ * (or instead of) the standard data.
+ * addData is not obligated, the function will just continue
+ * with the content setters if no addData is present.
+ *
+ * After the addData, all functions starting with "set" are called.
+ * These are used to set various parts of content.
*/
var g_SelectionPanels = {};
+// BARTER
+g_SelectionPanels.Barter = {
+ "maxNumberOfItems": 4,
+ "rowLength": 4,
+ "getItems": function(unitEntState, selection)
+ {
+ if (!unitEntState.barterMarket)
+ return [];
+ // ["food", "wood", "stone", "metal"]
+ return BARTER_RESOURCES;
+ },
+ "addData": function(data)
+ {
+ // data.item is the resource name in this case
+ data.button = {};
+ data.icon = {};
+ data.amount = {};
+ for (var a of BARTER_ACTIONS)
+ {
+ data.button[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Button["+data.i+"]");
+ data.icon[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Icon["+data.i+"]");
+ data.amount[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Amount["+data.i+"]");
+ }
+ data.selection = Engine.GetGUIObjectByName("unitBarterSellSelection["+data.i+"]");
+ data.affordableMask = Engine.GetGUIObjectByName("unitBarterSellUnaffordable["+data.i+"]");
+
+ data.amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL;
+ if (Engine.HotkeyIsPressed("session.massbarter"))
+ data.amountToSell *= BARTER_BUNCH_MULTIPLIER;
+ data.isSelected = data.item == g_barterSell;
+ return true;
+ },
+ "setCountDisplay": function(data)
+ {
+ data.amount.Sell.caption = "-" + data.amountToSell;
+ var sellPrice = data.unitEntState.barterMarket.prices["sell"][g_barterSell];
+ var buyPrice = data.unitEntState.barterMarket.prices["buy"][data.item];
+ data.amount.Buy.caption = "+" + Math.round(sellPrice / buyPrice * data.amountToSell);
+ },
+ "setTooltip": function(data)
+ {
+ data.button.Buy.tooltip = sprintf(translate("Buy %(resource)s"), {"resource": translate(data.item)});
+ data.button.Sell.tooltip = sprintf(translate("Sell %(resource)s"), {"resource": translate(data.item)});
+ },
+ "setAction": function(data)
+ {
+ data.button.Sell.onPress = function() { g_barterSell = data.item; };
+ var exchangeResourcesParameters = {
+ "sell": g_barterSell,
+ "buy": data.item,
+ "amount": data.amountToSell
+ };
+ data.button.Buy.onPress = function() { exchangeResources(exchangeResourcesParameters); };
+ },
+ "setGraphics": function(data)
+ {
+ var grayscale = data.isSelected ? "grayscale:" : "";
+ data.button.Buy.hidden = data.isSelected;
+ data.button.Sell.hidden = false;
+ for each (var icon in data.icon)
+ icon.sprite = "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png";
+
+ var neededRes = {};
+ neededRes[data.item] = data.amountToSell;
+ if (Engine.GuiInterfaceCall("GetNeededResources", neededRes))
+ data.affordableMask.hidden = false;
+ else
+ data.affordableMask.hidden = true;
+ data.selection.hidden = !data.isSelected;
+ },
+ "setPosition": function(data)
+ {
+ setPanelObjectPosition(data.button.Sell, data.i, data.rowLength);
+ setPanelObjectPosition(data.button.Buy, data.i + data.rowLength, data.rowLength);
+ },
+},
+
// COMMAND
g_SelectionPanels.Command = {
"maxNumberOfItems": 6,
+ "getItems": function(unitEntState)
+ {
+ return getEntityCommandsList(unitEntState)
+ },
"setTooltip": function(data)
{
if (data.item.tooltip)
data.button.tooltip = data.item.tooltip
else
data.button.tooltip = toTitleCase(data.item.name);
},
+ "setAction": function(data)
+ {
+ data.button.onPress = function() { data.item.callback ? data.item.callback(data.item) : performCommand(data.unitEntState.id, data.item.name); };
+ },
"setCountDisplay": function(data)
{
- var count = 0;
- if (data.item.name == "unload-all")
- count = data.garrisonGroups.getTotalCount();
- data.countDisplay.caption = count || "";
+ data.countDisplay.caption = data.item.count || "";
},
"setGraphics": function(data)
{
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
},
"setPosition": function(data)
{
var size = data.button.size;
// count on square buttons, so size.bottom is the width too
var spacer = size.bottom + 1;
// relative to the center ( = 50%)
size.rleft = size.rright = 50;
// offset from the center calculation
size.left = (data.i - data.numberOfItems/2) * spacer;
size.right = size.left + size.bottom;
data.button.size = size;
},
};
// CONSTRUCTION
g_SelectionPanels.Construction = {
"maxNumberOfItems": 24,
+ "conflictsWith": ["Gate", "Pack", "Training"],
+ "getItems": function()
+ {
+ return getAllBuildableEntitiesFromSelection();
+ },
"addData": function(data)
{
data.entType = data.item;
data.template = GetTemplateData(data.entType);
if (!data.template) // abort if no template
return false;
data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", data.template.requiredTechnology);
if (data.template.cost)
{
var totalCost = multiplyEntityCosts(data.template, 1);
data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", totalCost);
}
+ data.limits = getEntityLimitAndCount(data.playerState, data.entType);
return true;
},
+ "setAction": function(data)
+ {
+ data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); };
+ },
"setTooltip": function(data)
{
var tooltip = getEntityNamesFormatted(data.template);
tooltip += getVisibleEntityClassesFormatted(data.template);
if (data.template.tooltip)
tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]";
tooltip += "\n" + getEntityCostTooltip(data.template);
tooltip += getPopulationBonusTooltip(data.template);
- var limits = getEntityLimitAndCount(data.playerState, data.entType);
- data.entLimit = limits[0];
- data.entCount = limits[1];
- data.canBeAddedCount = limits[2];
- data.entLimitChangers = limits[3];
-
- tooltip += formatLimitString(data.entLimit, data.entCount, data.entLimitChangers);
+ tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers);
if (!data.technologyEnabled)
{
var techName = getEntityNames(GetTechnologyData(data.template.requiredTechnology));
tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { technology: techName });
}
if (data.neededResources)
tooltip += getNeededResourcesTooltip(data.neededResources);
data.button.tooltip = tooltip;
return true;
},
"setGraphics": function(data)
{
var grayscale = "";
- if (!data.technologyEnabled || data.canBeAddedCount == 0)
+ if (!data.technologyEnabled || data.limits.canBeAddedCount == 0)
{
data.button.enabled = false;
grayscale = "grayscale:";
data.affordableMask.hidden = false;
data.affordableMask.sprite = "colour: 0 0 0 127";
}
else if (data.neededResources)
{
data.button.enabled = false;
data.affordableMask.hidden = false;
data.affordableMask.sprite = resourcesToAlphaMask(data.neededResources);
}
if (data.template.icon)
data.icon.sprite = "stretched:" + grayscale + "session/portraits/" + data.template.icon;
},
};
// FORMATION
g_SelectionPanels.Formation = {
"maxNumberOfItems": 16,
"rowLength": 4,
+ "conflictsWith": ["Garrison"],
+ "getItems": function(unitEntState)
+ {
+ if (!hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
+ return [];
+ return Engine.GuiInterfaceCall("GetAvailableFormations");
+ },
"addData": function(data)
{
data.formationInfo = Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", {"templateName": data.item});
data.formationOk = canMoveSelectionIntoFormation(data.item);
data.formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": data.selection,
"formationTemplate": data.item
});
return true;
},
+ "setAction": function(data)
+ {
+ data.button.onPress = function() { performFormation(data.unitEntState.id, data.item); };
+ },
"setTooltip": function(data)
{
- var tooltip = translate(data.formationInfo.name);
+ var tooltip = translate(data.formationInfo.name);
if (!data.formationOk && data.formationInfo.tooltip)
tooltip += "\n" + "[color=\"red\"]" + translate(data.formationInfo.tooltip) + "[/color]";
data.button.tooltip = tooltip;
},
"setGraphics": function(data)
{
data.button.enabled = data.formationOk;
var grayscale = data.formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !data.formationSelected;
data.icon.sprite = "stretched:"+grayscale+"session/icons/"+data.formationInfo.icon;
},
};
// GARRISON
g_SelectionPanels.Garrison = {
"maxNumberOfItems": 12,
"rowLength": 4,
+ "getItems": function(unitEntState, selection)
+ {
+ if (!unitEntState.garrisonHolder)
+ return [];
+ var groups = new EntityGroups();
+ for (var ent of selection)
+ {
+ var state = GetEntityState(ent);
+ if (state.garrisonHolder)
+ groups.add(state.garrisonHolder.entities)
+ }
+ return groups.getEntsGrouped();
+ },
"addData": function(data)
{
- data.entType = data.item;
+ data.entType = data.item.template;
data.template = GetTemplateData(data.entType);
if (!data.template)
return false;
data.name = getEntityNames(data.template);
- data.count = data.garrisonGroups.getCount(data.item);
+ data.count = data.item.ents.length;
return true;
},
+ "setAction": function(data)
+ {
+ data.button.onPress = function() { unloadTemplate(data.item.template); };
+ },
"setTooltip": function(data)
{
var tooltip = sprintf(translate("Unload %(name)s"), { name: data.name }) + "\n";
tooltip += translate("Single-click to unload 1. Shift-click to unload all of this type.");
data.button.tooltip = tooltip;
},
"setCountDisplay": function(data)
{
data.countDisplay.caption = data.count || "";
},
"setGraphics": function(data)
{
var grayscale = "";
- var ents = data.garrisonGroups.getEntsByName(data.item);
+ var ents = data.item.ents;
var entplayer = GetEntityState(ents[0]).player;
data.button.sprite = "colour: " + rgbToGuiColor(g_Players[entplayer].color);
var player = Engine.GetPlayerID();
if(player != data.unitEntState.player && !g_DevSettings.controlAll)
{
if (data.entplayer != player)
{
data.button.enabled = false;
grayscale = "grayscale:";
}
}
data.icon.sprite = "stretched:" + grayscale + "session/portraits/" + data.template.icon;
},
};
// GATE
g_SelectionPanels.Gate = {
"maxNumberOfItems": 8,
+ "conflictsWith": ["Construction", "Pack", "Training"],
+ "getItems": function(unitEntState, selection)
+ {
+ if (unitEntState.foundation)
+ return [];
+ if (!hasClass(unitEntState, "LongWall") && !unitEntState.gate)
+ return [];
+ // Allow long wall pieces to be converted to gates
+ var longWallTypes = {};
+ var walls = [];
+ var gates = [];
+ for (var i in selection)
+ {
+ var state = GetEntityState(selection[i]);
+ if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template])
+ {
+ var gateTemplate = getWallGateTemplate(state.id);
+ if (gateTemplate)
+ {
+ var tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip;
+ if (!tooltipString)
+ {
+ warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template");
+ tooltipString = "";
+ }
+ walls.push({
+ "tooltip": translate(tooltipString),
+ "template": gateTemplate,
+ "callback": function (item) { transformWallToGate(item.template); }
+ });
+ }
+
+ // We only need one entity per type.
+ longWallTypes[state.template] = true;
+ }
+ else if (state.gate && !gates.length)
+ {
+ gates.push({
+ "gate": state.gate,
+ "tooltip": translate("Lock Gate"),
+ "locked": true,
+ "callback": function (item) { lockGate(item.locked); }
+ });
+ gates.push({
+ "gate": state.gate,
+ "tooltip": translate("Unlock Gate"),
+ "locked": false,
+ "callback": function (item) { lockGate(item.locked); }
+ });
+ }
+ // Show both 'locked' and 'unlocked' as active if the selected gates have both lock states.
+ else if (state.gate && state.gate.locked != gates[0].gate.locked)
+ for (var j = 0; j < gates.length; ++j)
+ delete gates[j].gate.locked;
+ }
+
+ // Place wall conversion options after gate lock/unlock icons.
+ var items = gates.concat(walls);
+ return items;
+ },
+ "setAction": function(data)
+ {
+ data.button.onPress = function() {data.item.callback(data.item); };
+ },
"setTooltip": function(data)
{
var tooltip = data.item.tooltip;
if (data.item.template)
{
data.template = GetTemplateData(data.item.template);
data.wallCount = data.selection.reduce(function (count, ent) {
var state = GetEntityState(ent);
if (hasClass(state, "LongWall") && !state.gate)
count++;
return count;
}, 0);
tooltip += "\n" + getEntityCostTooltip(data.template, data.wallCount);
data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", multiplyEntityCosts(data.template, data.wallCount));
if (data.neededResources)
tooltip += getNeededResourcesTooltip(data.neededResources);
}
data.button.tooltip = tooltip;
},
"setGraphics": function(data)
{
data.affordableMask.hidden == data.neededResources ? true : false;
var gateIcon;
if (data.item.gate)
{
// If already a gate, show locking actions
gateIcon = "icons/lock_" + GATE_ACTIONS[data.item.locked ? 0 : 1] + "ed.png";
if (data.item.gate.locked === undefined)
data.guiSelection.hidden = false
else
data.guiSelection.hidden = data.item.gate.locked != data.item.locked;
}
else
{
// otherwise show gate upgrade icon
var template = GetTemplateData(data.item.template);
if (!template)
return;
- gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png";
+ gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png";
data.guiSelection.hidden = true;
}
data.icon.sprite = "stretched:session/" + gateIcon;
},
};
// PACK
g_SelectionPanels.Pack = {
"maxNumberOfItems": 8,
+ "conflictsWith": ["Construction", "Gate", "Training"],
+ "getItems": function(unitEntState, selection)
+ {
+ if (!unitEntState.pack)
+ return [];
+ var checks = {};
+ for (var ent of selection)
+ {
+ var state = GetEntityState(ent);
+ if (!state.pack)
+ continue;
+ if (state.pack.progress == 0)
+ {
+ if (!state.pack.packed)
+ checks.packButton = true;
+ else if (state.pack.packed)
+ checks.unpackButton = true;
+ }
+ else
+ {
+ // Already un/packing - show cancel button
+ if (!state.pack.packed)
+ checks.packCancelButton = true;
+ else if (state.pack.packed)
+ checks.unpackCancelButton = true;
+ }
+ }
+ var 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;
+ },
+ "setAction": function(data)
+ {
+ data.button.onPress = function() {data.item.callback(data.item); };
+ },
"setTooltip": function(data)
{
data.button.tooltip = data.item.tooltip;
},
"setGraphics": function(data)
{
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";
},
};
// QUEUE
g_SelectionPanels.Queue = {
"maxNumberOfItems": 16,
+ "getItems": function(unitEntState)
+ {
+ if (!unitEntState.production)
+ return [];
+ return unitEntState.production.queue;
+ },
+ "resizePanel": function(numberOfItems, rowLength)
+ {
+ var numRows = Math.ceil(numberOfItems / rowLength);
+ var panel = Engine.GetGUIObjectByName("unitQueuePanel");
+ var size = panel.size;
+ size.top = (UNIT_PANEL_BASE - ((numRows-1)*UNIT_PANEL_HEIGHT));
+ panel.size = size;
+ },
"addData": function(data)
{
// differentiate between units and techs
if (data.item.unitTemplate)
{
data.entType = data.item.unitTemplate;
data.template = GetTemplateData(data.entType);
}
else if (data.item.technologyTemplate)
{
data.entType = data.item.technologyTemplate;
data.template = GetTechnologyData(data.entType);
}
data.progress = Math.round(data.item.progress*100) + "%";
return data.template;
},
+ "setAction": function(data)
+ {
+ data.button.onPress = function() { removeFromProductionQueue(data.unitEntState.id, data.item.id); };
+ },
"setTooltip": function(data)
{
var tooltip = getEntityNames(data.template);
if (data.item.neededSlots)
{
tooltip += "\n[color=\"red\"]" + translate("Insufficient population capacity:") + "\n[/color]";
tooltip += sprintf(translate("%(population)s %(neededSlots)s"), { population: getCostComponentDisplayName("population"), neededSlots: data.item.neededSlots });
}
data.button.tooltip = tooltip;
},
"setCountDisplay": function(data)
{
data.countDisplay.caption = data.item.count > 1 ? data.item.count : "";
if (data.i == 0)
{
// Also set the general progress display here
// maybe a separate method would be more appropriate
Engine.GetGUIObjectByName("queueProgress").caption = data.progress;
var guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider["+data.i+"]");
var 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(data.item.progress * (size.right - size.left));
guiObject.size = size;
}
},
"setGraphics": function(data)
{
if (data.template.icon)
data.icon.sprite = "stretched:session/portraits/" + data.template.icon;
},
};
// RESEARCH
g_SelectionPanels.Research = {
"maxNumberOfItems": 8,
+ "getItems": function(unitEntState, selection)
+ {
+ if (!unitEntState.production)
+ return [];
+ // TODO 8 is the row lenght, make variable
+ if (getNumberOfRightPanelButtons() > 8 && selection.length > 1)
+ return [];
+ return unitEntState.production.technologies;
+ },
"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;
},
"addData": function(data)
{
data.entType = data.item.pair ? [data.item.top, data.item.bottom] : [data.item];
data.template = data.entType.map(GetTechnologyData);
// abort if no template found for any of the techs
if (!data.template.every(function(v) { return v; }))
return false;
// index one row below
var shiftedIndex = data.i + data.rowLength;
data.positions = data.item.pair ? [data.i, shiftedIndex] : [shiftedIndex];
data.positionsToHide = data.item.pair ? [] : [data.i];
// add top buttons to the data
data.button = data.positions.map(function(p) {
return Engine.GetGUIObjectByName("unitResearchButton["+p+"]");
});
data.buttonsToHide = data.positionsToHide.map(function(p) {
return Engine.GetGUIObjectByName("unitResearchButton["+p+"]");
});
data.affordableMask = data.positions.map(function(p) {
return Engine.GetGUIObjectByName("unitResearchUnaffordable["+p+"]");
});
data.icon = data.positions.map(function(p) {
return Engine.GetGUIObjectByName("unitResearchIcon["+p+"]");
});
data.unchosenIcon = data.positions.map(function(p) {
return Engine.GetGUIObjectByName("unitResearchUnchosenIcon["+p+"]");
});
data.neededResources = data.template.map(function(t) {
return Engine.GuiInterfaceCall("GetNeededResources", t.cost);
});
data.requirementsPassed = data.entType.map(function(e) {
return Engine.GuiInterfaceCall("CheckTechnologyRequirements",e);
});
data.pair = Engine.GetGUIObjectByName("unitResearchPair["+data.i+"]");
return true;
},
"setTooltip": function(data)
{
for (var i in data.entType)
{
var tooltip = "";
var template = data.template[i];
tooltip = getEntityNamesFormatted(template);
if (template.tooltip)
tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]";
tooltip += "\n" + getEntityCostTooltip(template);
if (!data.requirementsPassed[i])
{
tooltip += "\n" + template.requirementsTooltip;
if (template.classRequirements)
{
var player = Engine.GetPlayerID();
var current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0;
var remaining = template.classRequirements.number - current;
tooltip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), { number: remaining});
}
}
if (data.neededResources[i])
tooltip += getNeededResourcesTooltip(data.neededResources[i]);
data.button[i].tooltip = tooltip;
}
},
- "setActions": function(data)
+ "setAction": function(data)
{
for (var i in data.entType)
{
// array containing the indices other buttons
var others = Object.keys(data.template);
others.splice(i, 1);
var button = data.button[i];
- button.onpress = (function(e){ return function() { data.callback(e) } })(data.entType[i]);
+ // as we're in a loop, we need to limit the scope with a closure
+ // else the last value of the loop will be taken, rather than the current one
+ button.onpress = (function(template) { return function () { addResearchToQueue(data.unitEntState.id, template); }; })(data.entType[i]);
// on mouse enter, show a cross over the other icons
button.onmouseenter = (function(others, icons) {
return function() {
for (var j of others)
icons[j].hidden = false;
};
})(others, data.unchosenIcon);
button.onmouseleave = (function(others, icons) {
return function() {
for (var j of others)
icons[j].hidden = true;
};
})(others, data.unchosenIcon);
}
},
"setGraphics": function(data)
{
for (var i in data.entType)
{
var button = data.button[i];
button.hidden = false;
var grayscale = "";
if (!data.requirementsPassed[i])
{
button.enabled = false;
grayscale = "grayscale:";
data.affordableMask[i].hidden = false;
data.affordableMask[i].sprite = "colour: 0 0 0 127";
}
else if (data.neededResources[i])
{
button.enabled = false;
data.affordableMask[i].hidden = false;
data.affordableMask[i].sprite = resourcesToAlphaMask(data.neededResources[i]);
}
else
{
data.affordableMask[i].hidden = true;
button.enabled = true;
}
if (data.template[i].icon)
data.icon[i].sprite = "stretched:" + grayscale + "session/portraits/" + data.template[i].icon;
}
- for (var button of data.buttonsToHide)
+ for (var button of data.buttonsToHide)
button.hidden = true;
// show the tech connector
data.pair.hidden = data.item.pair == null;
},
"setPosition": function(data)
{
for (var i in data.button)
setPanelObjectPosition(data.button[i], data.positions[i], data.rowLength);
setPanelObjectPosition(data.pair, data.i, data.rowLength);
},
};
// SELECTION
g_SelectionPanels.Selection = {
"maxNumberOfItems": 16,
"rowLength": 4,
+ "getItems": function(unitEntState, selection)
+ {
+ if (selection.length < 2)
+ return [];
+ return g_Selection.groups.getTemplateNames();
+ },
"addData": function(data)
{
data.entType = data.item;
data.template = GetTemplateData(data.entType);
if (!data.template)
return false;
data.name = getEntityNames(data.template);
data.count = g_Selection.groups.getCount(data.item);
return true;
},
"setTooltip": function(data)
{
data.button.tooltip = data.name;
},
"setCountDisplay": function(data)
{
data.countDisplay.caption = data.count || "";
},
- "setActions": function(data)
+ "setAction": function(data)
{
- data.button.onpressright = (function(e){return function() {data.callback(e, true) } })(data.item);
- data.button.onpress = (function(e){ return function() {data.callback(e, false) } })(data.item);
+ data.button.onpressright = function() { changePrimarySelectionGroup(data.item, true); };
+ data.button.onpress = function() { changePrimarySelectionGroup(data.item, false); };
},
"setGraphics": function(data)
{
if (data.template.icon)
data.icon.sprite = "stretched:session/portraits/" + data.template.icon;
},
};
// STANCE
g_SelectionPanels.Stance = {
"maxNumberOfItems": 5,
+ "getItems": function(unitEntState)
+ {
+ if (!unitEntState.unitAI || !hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal"))
+ return [];
+ return unitEntState.unitAI.possibleStances;
+ },
"addData": function(data)
{
data.stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": data.selection,
"stance": data.item
});
return true;
},
+ "setAction": function(data)
+ {
+ data.button.onPress = function() { performStance(data.unitEntState, data.item); };
+ },
"setTooltip": function(data)
{
data.button.tooltip = getStanceDisplayName(data.item);
},
"setGraphics": function(data)
{
data.guiSelection.hidden = !data.stanceSelected;
data.icon.sprite = "stretched:session/icons/stances/"+data.item+".png";
},
};
// TRAINING
g_SelectionPanels.Training = {
"maxNumberOfItems": 24,
+ "conflictsWith": ["Construction", "Gate", "Pack"],
+ "getItems": function()
+ {
+ return getAllTrainableEntitiesFromSelection();
+ },
"addData": function(data)
{
data.entType = data.item;
data.template = GetTemplateData(data.entType);
if (!data.template)
return false;
data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", data.template.requiredTechnology);
var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingBatchStatus(data.playerState, data.unitEntState.id, data.entType, data.selection);
data.buildingsCountToTrainFullBatch = buildingsCountToTrainFullBatch;
data.fullBatchSize = fullBatchSize;
data.remainderBatch = remainderBatch;
data.trainNum = 1;
if (Engine.HotkeyIsPressed("session.batchtrain"))
data.trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
if (data.template.cost)
{
var totalCosts = multiplyEntityCosts(data.template, data.trainNum);
data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", totalCosts);
}
return true;
},
+ "setAction": function(data)
+ {
+ data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); };
+ },
"setCountDisplay": function(data)
{
var count = "";
if (data.trainNum > 1)
count = data.trainNum;
data.countDisplay.caption = count;
},
"setTooltip": function(data)
{
var tooltip = "";
var key = Engine.ConfigDB_GetValue("user", "hotkey.session.queueunit." + (data.i + 1));
if (key)
tooltip += "[color=\"255 251 131\"][font=\"sans-bold-16\"][" + key + "][/font][/color] ";
tooltip += getEntityNamesFormatted(data.template);
tooltip += getVisibleEntityClassesFormatted(data.template);
if (data.template.auras)
{
for (var auraName in data.template.auras)
{
tooltip += "\n[font=\"sans-bold-13\"]" + translate(auraName) + "[/font]";
if (data.template.auras[auraName])
tooltip += ": " + translate(data.template.auras[auraName]);
}
}
if (data.template.tooltip)
tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]";
tooltip += "\n" + getEntityCostTooltip(data.template, data.trainNum, data.unitEntState.id);
- // TODO make the getEntityLimitAndCount method return something nicer than an array
- var limits = getEntityLimitAndCount(data.playerState, data.entType);
- data.entLimit = limits[0];
- data.entCount = limits[1];
- data.canBeAddedCount = limits[2];
- data.entLimitChangers = limits[3];
+ data.limits = getEntityLimitAndCount(data.playerState, data.entType);
- tooltip += formatLimitString(data.entLimit, data.entCount, data.entLimitChangers);
+ tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers);
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
{
if (data.template.health)
tooltip += "\n[font=\"sans-bold-13\"]" + translate("Health:") + "[/font] " + data.template.health;
if (data.template.attack)
tooltip += "\n" + getEntityAttack(data.template);
if (data.template.armour)
tooltip += "\n[font=\"sans-bold-13\"]" + translate("Armor:") + "[/font] " + armorTypesToText(data.template.armour);
if (data.template.speed)
tooltip += "\n" + getEntitySpeed(data.template);
}
tooltip += "[color=\"255 251 131\"]" + formatBatchTrainingString(data.buildingsCountToTrainFullBatch, data.fullBatchSize, data.remainderBatch) + "[/color]";
if (!data.technologyEnabled)
{
var techName = getEntityNames(GetTechnologyData(data.template.requiredTechnology));
tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { technology: techName });
}
if (data.neededResources)
tooltip += getNeededResourcesTooltip(data.neededResources);
data.button.tooltip = tooltip;
},
// disable and enable buttons in the same way as when you do for the construction
"setGraphics": g_SelectionPanels.Construction.setGraphics,
};
+
+
+/**
+ * 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.
+ */
+var 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
+ "Formation",
+ "Stance", // normal together with formation
+
+ // RIGHT PANE
+ "Gate", // must always be shown on gates
+ "Pack", // must always be shown on packable entities
+ "Training", // lets hope training and construction never happen together
+ "Construction",
+ "Research", // normal together with training
+
+ // UNIQUE PANES (importance doesn't matter)
+ "Command",
+ "Queue",
+ "Selection",
+];
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 15374)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 15375)
@@ -1,1211 +1,1208 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 15375)
@@ -1,876 +1,885 @@
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var unitActions =
{
"move":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
return {"possible": true};
},
"actionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
if (haveUnitAI && getActionInfo("move", target).possible)
return {"type": "move"};
return false;
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
return {"possible": true};
},
"hotkeyActionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
if (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible)
return {"type": "attack-move", "cursor": "action-attack-move"};
return false;
},
"specificness": 30,
},
"attack":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState.hitpoints)
return false;
if (playerCheck(entState, targetState, ["Neutral", "Enemy"]))
return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};
return false;
},
"hotkeyActionCheck": function(target)
{
if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible)
return {"type": "attack", "cursor": "action-attack", "target": target};
return false
},
"actionCheck": function(target)
{
if (getActionInfo("attack", target).possible)
return {"type": "attack", "cursor": "action-attack", "target": target};
return false;
},
"specificness": 10,
},
"heal":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.healer)
return false;
if (!hasClass(targetState, "Unit") || !targetState.needsHeal)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
// Healers can't heal themselves.
if (entState.id == targetState.id)
return false;
var unhealableClasses = entState.healer.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
var healableClasses = entState.healer.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return {"possible": true};
},
"actionCheck": function(target)
{
if (getActionInfo("heal", target).possible)
return {"type": "heal", "cursor": "action-heal", "target": target};
return false;
},
"specificness": 7,
},
"build":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (targetState.foundation && entState.buildEntities && playerCheck(entState, targetState, ["Player"]))
return {"possible": true};
return false;
},
"actionCheck": function(target)
{
if (getActionInfo("build", target).possible)
return {"type": "build", "cursor": "action-build", "target": target};
return false;
},
"specificness": 3,
},
"repair":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (entState.buildEntities && targetState.needsRepair && playerCheck(entState, targetState, ["Player"]))
return {"possible": true};
return false;
},
"preSelectedActionCheck" : function(target)
{
if (preSelectedAction != ACTION_REPAIR)
return false;
if (getActionInfo("repair", target).possible)
return {"type": "repair", "cursor": "action-repair", "target": target};
return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
},
"actionCheck": function(target)
{
if (getActionInfo("repair", target).possible)
return {"type": "build", "cursor": "action-repair", "target": target};
return false;
},
"specificness": 4,
},
"gather":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceSupply)
return false;
var resource = findGatherType(entState, targetState.resourceSupply);
if (resource)
return {"possible": true, "cursor": "action-gather-" + resource};
return false;
},
"actionCheck": function(target)
{
var actionInfo = getActionInfo("gather", target);
if (!actionInfo.possible)
return false;
return {"type": "gather", "cursor": actionInfo.cursor, "target": target};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceDropsite)
return false;
if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
var carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {"possible": true, "cursor": "action-return-" + carriedType};
},
"actionCheck": function(target)
{
var actionInfo = getActionInfo("returnresource", target);
if (!actionInfo.possible)
return false;
return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target, "source": undefined, "route": undefined, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (targetState.foundation || !entState.trader)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
if (!(hasClass(entState, "Organic") && hasClass(targetState, "Market")) && !(hasClass(entState, "Ship") && hasClass(targetState, "NavalMarket")))
return false;
var tradingData = {"trader": entState.id, "target": targetState.id};
var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData);
if (!tradingDetails)
return false;
var tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.");
if (tradingDetails.hasBothMarkets)
tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), {
gain: getTradingTooltip(tradingDetails.gain)
});
else
tooltip += "\n" + translate("Right-click on another market to set it as a destination trade market.")
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), {
gain: getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0) // markets too close
return false;
tooltip = translate("Right-click to set as destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), {
gain: getTradingTooltip(tradingDetails.gain)
});
break;
}
return {"possible": true, "tooltip": tooltip};
},
"actionCheck": function(target)
{
var actionInfo = getActionInfo("setup-trade-route", target);
if (!actionInfo.possible)
return false;
return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target};
},
"specificness": 0,
},
"garrison":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!hasClass(entState, "Unit") || !targetState.garrisonHolder)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
var tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount,
capacity: targetState.garrisonHolder.capacity
});
var extraCount = 0;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return {"possible": true, "tooltip": tooltip};
return false;
},
"preSelectedActionCheck": function(target)
{
if (preSelectedAction != ACTION_GARRISON)
return false;
var actionInfo = getActionInfo("garrison", target);
if (actionInfo.possible)
return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target};
return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined};
},
"hotkeyActionCheck": function(target)
{
var actionInfo = getActionInfo("garrison", target);
if (Engine.HotkeyIsPressed("session.garrison") && actionInfo.possible)
return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target};
return false;
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.guard)
return false;
if (!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
if (!entState.unitAI || !entState.unitAI.canGuard)
return false;
if (targetState.unitAI && targetState.unitAI.isGuarding)
return false;
return {"possible": true};
},
"preSelectedActionCheck" : function(target)
{
if (preSelectedAction != ACTION_GARRISON)
return false;
if (getActionInfo("guard", target).possible)
return {"type": "guard", "cursor": "action-guard", "target": target};
return {"type": "none", "cursor": "action-guard-disabled", "target": undefined};
},
"hotkeyActionCheck": function(target)
{
if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible)
return {"type": "guard", "cursor": "action-guard", "target": target};
return false;
},
"specificness": 40,
},
"remove-guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "remove-guard", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
return true;
},
"hotkeyActionCheck": function(target)
{
if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("remove-guard", target).possible)
{
var isGuarding = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI && entState.unitAI.isGuarding;
});
if (isGuarding)
return {"type": "remove-guard", "cursor": "action-remove-guard"};
}
return false;
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z, "data": action.data, "queued": queued});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
var tooltip;
// default to walking there (or attack-walking if hotkey pressed)
var data = {command: "walk"};
var cursor = "";
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data = {command: "attack-walk"};
cursor = "action-attack-move";
}
if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount,
capacity: targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.garrisonedEntitiesCount >= targetState.garrisonHolder.capacity)
tooltip = "[color=\"orange\"]" + tooltip + "[/color]";
}
else if (targetState.resourceSupply)
{
var resourceType = targetState.resourceSupply.type;
if (resourceType.generic == "treasure")
cursor = "action-gather-" + resourceType.generic;
else
cursor = "action-gather-" + resourceType.specific;
data.command = "gather";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
}
else if (targetState.foundation && entState.buildEntities)
{
data.command = "build";
data.target = targetState.id;
cursor = "action-build";
}
else if (hasClass(entState, "Market") && hasClass(targetState, "Market") && entState.id != targetState.id &&
(!hasClass(entState, "NavalMarket") || hasClass(targetState, "NavalMarket")) && !playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this building can produce.
var trader;
if (entState.production && entState.production.entities.length)
for (var i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
break;
var traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader };
var gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain && gain.traderGain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
tooltip = translate("Right-click to establish a default route for new traders.");
if (trader)
tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(gain) });
else // Foundation or cannot produce traders
tooltip += "\n" + sprintf(translate("Expected gain: %(gain)s"), { gain: getTradingTooltip(gain) });
}
}
else if (targetState.needsRepair && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities
var selection = g_Selection.toList();
if (!Engine.HotkeyIsPressed("session.autorallypoint") || !targetState.production || !targetState.production.entities.length)
{
for (var i = 0; i < selection.length; i++)
if (targetState.id === selection[i])
return false;
}
return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor, "tooltip": tooltip};
},
"actionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
if (haveUnitAI)
return false;
// Work out whether at least part the selection have rally points
// while none have UnitAI
var haveRallyPoints = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && ("rallyPoint" in entState) && entState.rallyPoint;
});
if (!haveRallyPoints)
return false;
var actionInfo = getActionInfo("set-rallypoint", target);
if (!actionInfo.possible)
return false;
return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (entState.id != targetState.id)
return false;
if (!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return {"possible": true};
},
"actionCheck": function(target, selection)
{
// Work out whether at least part of the selection have UnitAI
var haveUnitAI = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.unitAI;
});
// Work out whether at least part the selection have rally points
// while none have UnitAI
var haveRallyPoints = selection.some(function(ent) {
var entState = GetEntityState(ent);
return entState && ("rallyPoint" in entState) && entState.rallyPoint;
});
if (!haveUnitAI && haveRallyPoints && getActionInfo("unset-rallypoint", target).possible)
return {"type": "unset-rallypoint", "cursor": "action-unset-rally"};
return false;
},
"specificness": 11,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central pane
*/
var entityCommands =
{
// Unload
"unload-all": {
"getInfo": function(entState)
{
if (!entState.garrisonHolder)
return false;
+ var selection = g_Selection.toList();
+ var count = 0;
+ for (var ent of selection)
+ {
+ var state = GetEntityState(ent);
+ if (state.garrisonHolder)
+ count += state.garrisonHolder.entities.length;
+ }
return {
"tooltip": translate("Unload All"),
- "icon": "garrison-out.png"
+ "icon": "garrison-out.png",
+ "count": count,
};
},
"execute": function(entState)
{
unloadAll();
},
},
// Delete
"delete": {
"getInfo": function(entState)
{
return {
"tooltip": translate("Delete"),
"icon": "kill_small.png"
};
},
"execute": function(entState)
{
var selection = g_Selection.toList();
if (selection.length < 1)
return;
if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather)
openDeleteDialog(selection);
},
},
// Stop
"stop": {
"getInfo": function(entState)
{
if (!entState.unitAI)
return false;
return {
"tooltip": translate("Stop"),
"icon": "stop.png"
};
},
"execute": function(entState)
{
var selection = g_Selection.toList();
if (selection.length > 0)
stopUnits(selection);
},
},
// Garrison
"garrison": {
"getInfo": function(entState)
{
if (!entState.unitAI || entState.turretParent)
return false;
return {
"tooltip": translate("Garrison"),
"icon": "garrison.png"
};
},
"execute": function(entState)
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
},
// Ungarrison
"unload": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.turretParent)
return false;
var p = GetEntityState(entState.turretParent);
if (p.garrisonHolder || p.garrisonHolder.entities.indexOf(entState.id) == -1)
return false;
return {
"tooltip": translate("Unload"),
"icon": "garrison-out.png"
};
},
"execute": function(entState)
{
unloadSelection();
},
},
// Repair
"repair": {
"getInfo": function(entState)
{
if (!entState.buildEntities)
return false;
return {
"tooltip": translate("Repair"),
"icon": "repair.png"
};
},
"execute": function(entState)
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
},
// Focus on rally point
"focus-rally": {
"getInfo": function(entState)
{
if (!entState.rallyPoint)
return false;
return {
"tooltip": translate("Focus on Rally Point"),
"icon": "focus-rally.png"
};
},
"execute": function(entState)
{
var focusTarget = null;
if (entState.rallyPoint && entState.rallyPoint.position)
focusTarget = entState.rallyPoint.position;
else if (entState.position)
focusTarget = entState.position;
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
},
// Back to work
"back-to-work": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.unitAI.hasWorkOrders)
return false;
return {
"tooltip": translate("Back to Work"),
"icon": "production.png"
};
},
"execute": function(entState)
{
backToWork();
},
},
// Guard
"add-guard": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding)
return false;
return {
"tooltip": translate("Guard"),
"icon": "add-guard.png"
};
},
"execute": function(entState)
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
},
// Remove guard
"remove-guard": {
"getInfo": function(entState)
{
if (!entState.unitAI || !entState.unitAI.isGuarding)
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png"
};
},
"execute": function(entState)
{
removeGuard();
},
},
// Trading
"select-trading-goods": {
"getInfo": function(entState)
{
if (!hasClass(entState, "Market"))
return false;
return {
"tooltip": translate("Select trading goods"),
"icon": "economics.png"
};
},
"execute": function(entState)
{
toggleTrade();
},
},
// Raise alert
"increase-alert-level": {
"getInfo": function(entState)
{
if(!entState.alertRaiser || !entState.alertRaiser.canIncreaseLevel)
return false;
if(entState.alertRaiser.hasRaisedAlert)
var tooltip = translate("Increase the alert level to protect more units");
else
var tooltip = translate("Raise an alert!");
return {
"tooltip": tooltip,
"icon": "bell_level1.png"
};
},
"execute": function(entState)
{
increaseAlertLevel();
},
},
// End alert
"alert-end": {
"getInfo": function(entState)
{
if(!entState.alertRaiser || !entState.alertRaiser.hasRaisedAlert)
return false
return {
"tooltip": translate("End of alert."),
"icon": "bell_level0.png"
};
},
"execute": function(entState)
{
endOfAlert();
},
},
};
function playerCheck(entState, targetState, validPlayers)
{
var playerState = GetSimState().players[entState.player];
for (var player of validPlayers)
{
if (player == "Gaia" && targetState.player == 0)
return true;
if (player == "Player" && targetState.player == entState.player)
return true;
if (playerState["is"+player] && playerState["is"+player][targetState.player])
return true;
}
return false;
};
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 15375)
@@ -1,733 +1,459 @@
// Panel types
const SELECTION = "Selection";
const QUEUE = "Queue";
const GARRISON = "Garrison";
const FORMATION = "Formation";
const TRAINING = "Training";
const RESEARCH = "Research";
const CONSTRUCTION = "Construction";
const COMMAND = "Command";
const STANCE = "Stance";
const GATE = "Gate";
const PACK = "Pack";
// Constants
const UNIT_PANEL_BASE = -52; // QUEUE: The offset above the main panel (will often be negative)
const UNIT_PANEL_HEIGHT = 44; // QUEUE: The height needed for a row of buttons
// Trading constants
-const TRADING_RESOURCES = ["food", "wood", "stone", "metal"];
+const TRADING_RESOURCES = [
+ markForTranslation("food"),
+ markForTranslation("wood"),
+ markForTranslation("stone"),
+ markForTranslation("metal")
+];
// Barter constants
const BARTER_RESOURCE_AMOUNT_TO_SELL = 100;
const BARTER_BUNCH_MULTIPLIER = 5;
const BARTER_RESOURCES = ["food", "wood", "stone", "metal"];
const BARTER_ACTIONS = ["Sell", "Buy"];
// Gate constants
const GATE_ACTIONS = ["lock", "unlock"];
// The number of currently visible buttons (used to optimise showing/hiding)
var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Research": 0, "Barter": 0, "Construction": 0, "Command": 0, "Stance": 0, "Gate": 0, "Pack": 0};
-// Unit panels are panels with row(s) of buttons
-var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Barter", "Construction", "Research", "Stance", "Command", "Gate", "Pack"];
-
-// Indexes of resources to sell and buy on barter panel
-var g_barterSell = 0;
+// Resources to sell on barter panel
+var g_barterSell = "food";
+/**
+ * Set the position of a panel object according to the index,
+ * from left to right, from top to bottom.
+ * Will wrap around to subsequent rows if the index
+ * is larger than rowLength.
+ */
function setPanelObjectPosition(object, index, rowLength, vMargin = 1, hMargin = 1)
{
var size = object.size;
// horizontal position
var oWidth = size.right - size.left;
var hIndex = index % rowLength;
size.left = hIndex * (oWidth + vMargin);
size.right = size.left + oWidth;
// vertical position
var oHeight = size.bottom - size.top;
var vIndex = Math.floor(index / rowLength);
size.top = vIndex * (oHeight + hMargin);
size.bottom = size.top + oHeight;
object.size = size;
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
if (trainEntLimit == undefined)
return "";
var text = "\n\n" + sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), { count: trainEntCount, limit: trainEntLimit });
if (trainEntCount >= trainEntLimit)
text = "[color=\"red\"]" + text + "[/color]";
for (var c in trainEntLimitChangers)
{
if (trainEntLimitChangers[c] > 0)
text += "\n" + sprintf(translate("%(changer)s enlarges the limit with %(change)s."), { changer: translate(c), change: trainEntLimitChangers[c] });
else if (trainEntLimitChangers[c] < 0)
text += "\n" + sprintf(translate("%(changer)s lessens the limit with %(change)s."), { changer: translate(c), change: (-trainEntLimitChangers[c]) });
}
return 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 "";
var batchTrainingString = "";
var fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 0)
{
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), {
buildings: buildingsCountToTrainFullBatch,
batchSize: fullBatchSize
});
else
fullBatchesString = fullBatchSize;
}
var remainderBatchString = remainderBatch > 0 ? remainderBatch : "";
var batchDetailsString = "";
var action = "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font][font=\"sans-13\"]"
// We need to display the batch details part if there is either more than
// one building with full batch or one building with the full batch and
// another with a partial batch
if (buildingsCountToTrainFullBatch > 1 ||
(buildingsCountToTrainFullBatch == 1 && remainderBatch > 0))
{
if (remainderBatch > 0)
return "\n[font=\"sans-13\"]" + sprintf(translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s)."), {
action: action,
number: totalBatchTrainingCount,
fullBatch: fullBatchesString,
remainderBatch: remainderBatch
}) + "[/font]";
return "\n[font=\"sans-13\"]" + sprintf(translate("%(action)s to train %(number)s (%(fullBatch)s)."), {
action: action,
number: totalBatchTrainingCount,
fullBatch: fullBatchesString
}) + "[/font]";
}
return "\n[font=\"sans-13\"]" + sprintf(translate("%(action)s to train %(number)s."), {
action: action,
number: totalBatchTrainingCount
}) + "[/font]";
}
function getStanceDisplayName(name)
{
var displayName;
switch(name)
{
case "violent":
displayName = translateWithContext("stance", "Violent");
break;
case "aggressive":
displayName = translateWithContext("stance", "Aggressive");
break;
case "passive":
displayName = translateWithContext("stance", "Passive");
break;
case "defensive":
displayName = translateWithContext("stance", "Defensive");
break;
case "standground":
displayName = translateWithContext("stance", "Standground");
break;
default:
warn(sprintf("Internationalization: Unexpected stance found with code ‘%(stance)s’. This stance must be internationalized.", { stance: name }));
displayName = name;
break;
}
return displayName;
}
/**
* Helper function for updateUnitCommands; sets up "unit panels" (i.e. panels with rows of icons) for the currently selected
* unit.
*
* @param guiName Short identifier string of this panel; see constants defined at the top of this file.
- * @param usedPanels Output object; usedPanels[guiName] will be set to 1 to indicate that this panel was used during this
- * run of updateUnitCommands and should not be hidden. TODO: why is this done this way instead of having
- * updateUnitCommands keep track of this?
* @param unitEntState Entity state of the (first) selected unit.
- * @param items Panel-specific data to construct the icons with.
- * @param callback Callback function to argument to execute when an item's icon gets clicked. Takes a single 'item' argument.
*/
-function setupUnitPanel(guiName, usedPanels, unitEntState, playerState, items, callback)
+function setupUnitPanel(guiName, unitEntState, playerState)
{
if (!g_SelectionPanels[guiName])
{
error("unknown guiName used '" + guiName + "'");
return;
}
- usedPanels[guiName] = 1;
+ var selection = g_Selection.toList();
+
+ var items = g_SelectionPanels[guiName].getItems(unitEntState, selection);
+
+ if (!items || !items.length)
+ return;
var numberOfItems = items.length;
- var selection = g_Selection.toList();
var garrisonGroups = new EntityGroups();
// Determine how many buttons there should be
if (g_SelectionPanels[guiName].maxNumberOfItems)
numberOfItems = Math.min(g_SelectionPanels[guiName].maxNumberOfItems, numberOfItems);
if (g_SelectionPanels[guiName].rowLength)
var rowLength = g_SelectionPanels[guiName].rowLength;
else
var rowLength = 8;
- // TODO get this out of here
- // Common code for garrison and 'unload all' button counts.
- if (guiName == GARRISON || guiName == COMMAND)
- {
- for (var i = 0; i < selection.length; ++i)
- {
- var state = GetEntityState(selection[i]);
- if (state.garrisonHolder)
- garrisonGroups.add(state.garrisonHolder.entities)
- }
- }
- // Resize Queue panel if needed
- if (guiName == QUEUE) // or garrison
- {
- var numRows = Math.ceil(numberOfItems / rowLength);
- var panel = Engine.GetGUIObjectByName("unitQueuePanel");
- var size = panel.size;
- size.top = (UNIT_PANEL_BASE - ((numRows-1)*UNIT_PANEL_HEIGHT));
- panel.size = size;
- }
+ if (g_SelectionPanels[guiName].resizePanel)
+ g_SelectionPanels[guiName].resizePanel(numberOfItems, rowLength);
// Make buttons
for (var i = 0; i < numberOfItems; i++)
{
var item = items[i];
// If a tech has been researched it leaves an empty slot
- if (!item && g_SelectionPanels[guiName].hideItem)
+ if (!item)
{
- g_SelectionPanels[guiName].hideItem(i, rowLength);
+ if (g_SelectionPanels[guiName].hideItem)
+ g_SelectionPanels[guiName].hideItem(i, rowLength);
continue;
}
// STANDARD DATA
// add standard data
var data = {
"i": i,
"item": item,
"selection": selection,
"playerState": playerState,
"unitEntState": unitEntState,
- "callback": callback,
"rowLength": rowLength,
"numberOfItems": numberOfItems,
};
- if (garrisonGroups)
- data.garrisonGroups = garrisonGroups;
-
// add standard gui objects to the data
// depending on the actual XML, some of this may be undefined
data.button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]");
data.affordableMask = Engine.GetGUIObjectByName("unit"+guiName+"Unaffordable["+i+"]");
data.icon = Engine.GetGUIObjectByName("unit"+guiName+"Icon["+i+"]");
data.guiSelection = Engine.GetGUIObjectByName("unit"+guiName+"Selection["+i+"]");
data.countDisplay = Engine.GetGUIObjectByName("unit"+guiName+"Count["+i+"]");
// DEFAULTS
- data.button.hidden = false;
- data.button.enabled = true;
- data.button.tooltip = "";
- data.button.caption = "";
-
- // Items can have a callback element that overrides the normal
- // caller-supplied callback function. Button Function
- // (need nested functions to get the closure right)
- data.button.onpress = (function(e){ return function() { e.callback ? e.callback(e) : callback(e) } })(item);
+ if (data.button)
+ {
+ data.button.hidden = false;
+ data.button.enabled = true;
+ data.button.tooltip = "";
+ data.button.caption = "";
+ }
if (data.affordableMask)
data.affordableMask.hidden = true; // actually used for the red "lack of resource" overlay, and darkening if unavailable. Sort of a hack.
// GENERAL DATA
// add general data, and a chance to abort on faulty data
if (g_SelectionPanels[guiName].addData)
{
var success = g_SelectionPanels[guiName].addData(data);
if (!success)
continue; // ignore faulty data
}
// SET CONTENT
// run all content setters
for (var f in g_SelectionPanels[guiName])
{
if (f.match(/^set/))
g_SelectionPanels[guiName][f](data);
}
// Special case: position
if (!g_SelectionPanels[guiName].setPosition)
setPanelObjectPosition(data.button, i, rowLength);
// TODO: we should require all entities to have icons, so this case never occurs
- if (!data.icon.sprite)
+ if (data.icon && !data.icon.sprite)
data.icon.sprite = "bkFillBlack";
}
// Hide any buttons we're no longer using
for (var i = numberOfItems; i < g_unitPanelButtons[guiName]; ++i)
if (g_SelectionPanels[guiName].hideItem)
g_SelectionPanels[guiName].hideItem(i, rowLength);
else
Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true;
// remember the number of items
g_unitPanelButtons[guiName] = numberOfItems;
+ g_SelectionPanels[guiName].used = true;
}
function resourcesToAlphaMask(neededResources)
{
var totalCost = 0;
for each (var resource in neededResources)
totalCost += resource;
var alpha = 50 + Math.round(totalCost/10);
alpha = alpha > 125 ? 125 : alpha;
return "colour: 255 0 0 " + (alpha);
}
-// Sets up "unit barter panel" - special case for setupUnitPanel
-function setupUnitBarterPanel(unitEntState, playerState)
-{
- // Amount of player's resource to exchange
- var amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL;
- if (Engine.HotkeyIsPressed("session.massbarter"))
- amountToSell *= BARTER_BUNCH_MULTIPLIER;
- // One pass for each resource
- for (var i = 0; i < BARTER_RESOURCES.length; i++)
- {
- var resource = BARTER_RESOURCES[i];
- // One pass for 'sell' row and another for 'buy'
- for (var j = 0; j < 2; j++)
- {
- var action = BARTER_ACTIONS[j];
-
- if (j == 0)
- {
- // Display the selection overlay
- var selection = Engine.GetGUIObjectByName("unitBarter" + action + "Selection["+i+"]");
- selection.hidden = !(i == g_barterSell);
- }
-
- // We gray out the not selected icons in 'sell' row
- var grayscale = (j == 0 && i != g_barterSell) ? "grayscale:" : "";
- var icon = Engine.GetGUIObjectByName("unitBarter" + action + "Icon["+i+"]");
-
- var button = Engine.GetGUIObjectByName("unitBarter" + action + "Button["+i+"]");
- button.size = (i * 46) + " 0 " + ((i + 1) * 46) + " 46";
- var amountToBuy;
- // We don't display a button in 'buy' row if the same resource is selected in 'sell' row
- if (j == 1 && i == g_barterSell)
- {
- button.hidden = true;
- }
- else
- {
- button.hidden = false;
- button.tooltip = action + " " + resource;
- icon.sprite = "stretched:"+grayscale+"session/icons/resources/" + resource + ".png";
- var sellPrice = unitEntState.barterMarket.prices["sell"][BARTER_RESOURCES[g_barterSell]];
- var buyPrice = unitEntState.barterMarket.prices["buy"][resource];
- amountToBuy = "+" + Math.round(sellPrice / buyPrice * amountToSell);
- }
-
- var amount;
- if (j == 0)
- {
- button.onpress = (function(i){ return function() { g_barterSell = i; } })(i);
- if (i == g_barterSell)
- {
- amount = "-" + amountToSell;
-
- var neededRes = {};
- neededRes[resource] = amountToSell;
- var neededResources = Engine.GuiInterfaceCall("GetNeededResources", neededRes);
- var hidden = neededResources ? false : true;
- for (var ii = 0; ii < BARTER_RESOURCES.length; ii++)
- {
- var affordableMask = Engine.GetGUIObjectByName("unitBarterBuyUnaffordable["+ii+"]");
- affordableMask.hidden = hidden;
- }
- }
- else
- amount = "";
- }
- else
- {
- var exchangeResourcesParameters = { "sell": BARTER_RESOURCES[g_barterSell], "buy": BARTER_RESOURCES[i], "amount": amountToSell };
- button.onpress = (function(exchangeResourcesParameters){ return function() { exchangeResources(exchangeResourcesParameters); } })(exchangeResourcesParameters);
- amount = amountToBuy;
- }
- Engine.GetGUIObjectByName("unitBarter" + action + "Amount["+i+"]").caption = amount;
- }
- }
-}
-
/**
* Updates the right hand side "Unit Commands" panel. Runs in the main session loop via updateSelectionDetails().
* Delegates to setupUnitPanel to set up individual subpanels, appropriately activated depending on the selected
* unit's state.
*
* @param entState Entity state of the (first) selected unit.
* @param supplementalDetailsPanel Reference to the "supplementalSelectionDetails" GUI Object
* @param commandsPanel Reference to the "commandsPanel" GUI Object
* @param selection Array of currently selected entity IDs.
*/
function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
{
- // Panels that are active
- var usedPanels = {};
+ for each (var panel in g_SelectionPanels)
+ panel.used = false;
// If the selection is friendly units, add the command panels
var player = Engine.GetPlayerID();
// Get player state to check some constraints
// e.g. presence of a hero or build limits
var simState = GetSimState();
var playerState = simState.players[player];
if (entState.player == player || g_DevSettings.controlAll)
{
- if (selection.length > 1)
- setupUnitPanel(SELECTION, usedPanels, entState, playerState, g_Selection.groups.getTemplateNames(),
- function (entType, rightPressed) { changePrimarySelectionGroup(entType, rightPressed); } );
-
- var commands = getEntityCommandsList(entState);
- if (commands.length)
- setupUnitPanel(COMMAND, usedPanels, entState, playerState, commands,
- function (item) { performCommand(entState.id, item.name); } );
-
- if (entState.garrisonHolder)
- {
- var groups = new EntityGroups();
- for (var i in selection)
- {
- var state = GetEntityState(selection[i]);
- if (state.garrisonHolder)
- groups.add(state.garrisonHolder.entities)
- }
-
- setupUnitPanel(GARRISON, usedPanels, entState, playerState, groups.getTemplateNames(),
- function (item) { unloadTemplate(item); } );
- }
-
- var formations = Engine.GuiInterfaceCall("GetAvailableFormations");
- if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && !entState.garrisonHolder && formations.length)
- {
- setupUnitPanel(FORMATION, usedPanels, entState, playerState, formations,
- function (item) { performFormation(entState.id, item); } );
- }
-
- // TODO: probably should load the stance list from a data file,
- // and/or vary depending on what units are selected
- var stances = ["violent", "aggressive", "passive", "defensive", "standground"];
- if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && stances.length)
- {
- setupUnitPanel(STANCE, usedPanels, entState, playerState, stances,
- function (item) { performStance(entState.id, item); } );
- }
-
- Engine.GetGUIObjectByName("unitBarterPanel").hidden = !entState.barterMarket;
- if (entState.barterMarket)
+ for (var guiName of g_PanelsOrder)
{
- usedPanels["Barter"] = 1;
- setupUnitBarterPanel(entState, playerState);
- }
-
- var buildableEnts = getAllBuildableEntitiesFromSelection();
- var trainableEnts = getAllTrainableEntitiesFromSelection();
-
- // Whether the GUI's right panel has been filled.
- var rightUsed = true;
+ if (
+ g_SelectionPanels[guiName].conflictsWith &&
+ g_SelectionPanels[guiName].conflictsWith.some(function (p) { return g_SelectionPanels[p].used; })
+ )
+ continue;
- // The first selected entity's type has priority.
- if (entState.buildEntities)
- setupUnitPanel(CONSTRUCTION, usedPanels, entState, playerState, buildableEnts,
- function (trainEntType) { startBuildingPlacement(trainEntType, playerState); } );
- else if (entState.production && entState.production.entities)
- setupUnitPanel(TRAINING, usedPanels, entState, playerState, trainableEnts,
- function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } );
- else if (!entState.foundation && entState.gate || hasClass(entState, "LongWall"))
- {
- // Allow long wall pieces to be converted to gates
- var longWallTypes = {};
- var walls = [];
- var gates = [];
- for (var i in selection)
- {
- var state = GetEntityState(selection[i]);
- if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template])
- {
- var gateTemplate = getWallGateTemplate(state.id);
- if (gateTemplate)
- {
- var tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip;
- if (!tooltipString)
- {
- warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template");
- tooltipString = "";
- }
- walls.push({
- "tooltip": translate(tooltipString),
- "template": gateTemplate,
- "callback": function (item) { transformWallToGate(item.template); }
- });
- }
-
- // We only need one entity per type.
- longWallTypes[state.template] = true;
- }
- else if (state.gate && !gates.length)
- {
- gates.push({
- "gate": state.gate,
- "tooltip": translate("Lock Gate"),
- "locked": true,
- "callback": function (item) { lockGate(item.locked); }
- });
- gates.push({
- "gate": state.gate,
- "tooltip": translate("Unlock Gate"),
- "locked": false,
- "callback": function (item) { lockGate(item.locked); }
- });
- }
- // Show both 'locked' and 'unlocked' as active if the selected gates have both lock states.
- else if (state.gate && state.gate.locked != gates[0].gate.locked)
- for (var j = 0; j < gates.length; ++j)
- delete gates[j].gate.locked;
- }
-
- // Place wall conversion options after gate lock/unlock icons.
- var items = gates.concat(walls);
- if (items.length)
- setupUnitPanel(GATE, usedPanels, entState, playerState, items);
- else
- rightUsed = false;
- }
- else if (entState.pack)
- {
- var items = [];
- var packButton = false;
- var unpackButton = false;
- var packCancelButton = false;
- var unpackCancelButton = false;
- for (var i in selection)
- {
- // Find un/packable entities
- var state = GetEntityState(selection[i]);
- if (state.pack)
- {
- if (state.pack.progress == 0)
- {
- if (!state.pack.packed)
- packButton = true;
- else if (state.pack.packed)
- unpackButton = true;
- }
- else
- {
- // Already un/packing - show cancel button
- if (!state.pack.packed)
- packCancelButton = true;
- else if (state.pack.packed)
- unpackCancelButton = true;
- }
- }
- }
- if (packButton)
- items.push({ "packing": false, "packed": false, "tooltip": translate("Pack"), "callback": function() { packUnit(true); } });
- if (unpackButton)
- items.push({ "packing": false, "packed": true, "tooltip": translate("Unpack"), "callback": function() { packUnit(false); } });
- if (packCancelButton)
- items.push({ "packing": true, "packed": false, "tooltip": translate("Cancel Packing"), "callback": function() { cancelPackUnit(true); } });
- if (unpackCancelButton)
- items.push({ "packing": true, "packed": true, "tooltip": translate("Cancel Unpacking"), "callback": function() { cancelPackUnit(false); } });
-
- if (items.length)
- setupUnitPanel(PACK, usedPanels, entState, playerState, items);
- else
- rightUsed = false;
+ setupUnitPanel(guiName, entState, playerState);
}
- else
- rightUsed = false;
-
- if (!rightUsed)
- {
- // The right pane is empty. Fill the pane with a sane type.
- // Prefer buildables for units and trainables for structures.
- if (buildableEnts.length && (hasClass(entState, "Unit") || !trainableEnts.length))
- setupUnitPanel(CONSTRUCTION, usedPanels, entState, playerState, buildableEnts,
- function (trainEntType) { startBuildingPlacement(trainEntType, playerState); });
- else if (trainableEnts.length)
- setupUnitPanel(TRAINING, usedPanels, entState, playerState, trainableEnts,
- function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } );
- }
- // Show technologies if the active panel has at most one row of icons.
- if (entState.production && entState.production.technologies.length)
- {
- var activepane = usedPanels[CONSTRUCTION] ? buildableEnts.length : trainableEnts.length;
- if (selection.length == 1 || activepane <= 8)
- setupUnitPanel(RESEARCH, usedPanels, entState, playerState, entState.production.technologies,
- function (researchType) { addResearchToQueue(entState.id, researchType); } );
- }
-
- if (entState.production && entState.production.queue.length)
- setupUnitPanel(QUEUE, usedPanels, entState, playerState, entState.production.queue,
- function (item) { removeFromProductionQueue(entState.id, item.id); } );
supplementalDetailsPanel.hidden = false;
commandsPanel.hidden = false;
}
else if (playerState.isMutualAlly[entState.player]) // owned by allied player
{
- if (entState.garrisonHolder)
- {
- var groups = new EntityGroups();
- for (var i in selection)
- {
- var state = GetEntityState(selection[i]);
- if (state.garrisonHolder)
- groups.add(state.garrisonHolder.entities)
- }
-
- setupUnitPanel(GARRISON, usedPanels, entState, playerState, groups.getTemplateNames(),
- function (item) { unloadTemplate(item); } );
-
+ // TODO if there's a second panel needed for a different player
+ // we should consider adding the players list to g_SelectionPanels
+ setupUnitPanel(GARRISON, entState, playerState);
+ if (g_SelectionPanels["Garrison"].used)
supplementalDetailsPanel.hidden = false;
- }
else
- {
supplementalDetailsPanel.hidden = true;
- }
commandsPanel.hidden = true;
}
else // owned by another player
{
supplementalDetailsPanel.hidden = true;
commandsPanel.hidden = true;
}
// Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time)
- var offset = 0;
- for each (var panelName in g_unitPanels)
- {
- var panel = Engine.GetGUIObjectByName("unit" + panelName + "Panel");
- if (usedPanels[panelName])
- panel.hidden = false;
- else
- panel.hidden = true;
- }
+ for (var panelName in g_SelectionPanels)
+ Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = !g_SelectionPanels[panelName].used;
}
// Force hide commands panels
function hideUnitCommands()
{
- for each (var panelName in g_unitPanels)
+ for (var panelName in g_SelectionPanels)
Engine.GetGUIObjectByName("unit" + panelName + "Panel").hidden = true;
}
// Get all of the available entities which can be trained by the selected entities
function getAllTrainableEntities(selection)
{
var trainableEnts = [];
var state;
// Get all buildable and trainable entities
for (var i in selection)
{
if ((state = GetEntityState(selection[i])) && state.production && state.production.entities.length)
trainableEnts = trainableEnts.concat(state.production.entities);
}
// Remove duplicates
removeDupes(trainableEnts);
return trainableEnts;
}
function getAllTrainableEntitiesFromSelection()
{
if (!g_allTrainableEntities)
g_allTrainableEntities = getAllTrainableEntities(g_Selection.toList());
return g_allTrainableEntities;
}
// Get all of the available entities which can be built by the selected entities
function getAllBuildableEntities(selection)
{
var buildableEnts = [];
var state;
// Get all buildable entities
for (var i in selection)
{
if ((state = GetEntityState(selection[i])) && state.buildEntities && state.buildEntities.length)
buildableEnts = buildableEnts.concat(state.buildEntities);
}
// Remove duplicates
removeDupes(buildableEnts);
return buildableEnts;
}
function getAllBuildableEntitiesFromSelection()
{
if (!g_allBuildableEntities)
g_allBuildableEntities = getAllBuildableEntities(g_Selection.toList());
return g_allBuildableEntities;
}
// Check if the selection can move into formation, and cache the result
function canMoveSelectionIntoFormation(formationTemplate)
{
if (!(formationTemplate in g_canMoveIntoFormation))
{
g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationTemplate": formationTemplate
});
}
return g_canMoveIntoFormation[formationTemplate];
}
function getVisibleEntityClassesFormatted(template)
{
var r = ""
if (template.visibleIdentityClasses && template.visibleIdentityClasses.length)
{
r += "\n[font=\"sans-bold-13\"]" + translate("Classes:") + "[/font] ";
r += "[font=\"sans-13\"]" + translate(template.visibleIdentityClasses[0]) ;
for (var c = 1; c < template.visibleIdentityClasses.length; c++)
r += ", " + translate(template.visibleIdentityClasses[c]);
r += "[/font]";
}
return r;
-};
+}
+
+function getNumberOfRightPanelButtons()
+{
+ var sum = 0;
+ if (g_SelectionPanels["Construction"].used)
+ sum += g_unitPanelButtons["Construction"];
+ if (g_SelectionPanels["Training"].used)
+ sum += g_unitPanelButtons["Training"];
+ if (g_SelectionPanels["Pack"].used)
+ sum += g_unitPanelButtons["Pack"];
+ if (g_SelectionPanels["Gate"].used)
+ sum += g_unitPanelButtons["Gate"];
+ return sum;
+}
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 15375)
@@ -1,1971 +1,1972 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised so we mustn't serialise
// its non-deterministic data. Instead just return an empty object.
return {};
};
GuiInterface.prototype.Deserialize = function(obj)
{
this.Init();
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
};
/*
* 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(player)
{
var ret = {
"players": []
};
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var n = cmpPlayerMan.GetNumPlayers();
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
var phase = "";
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
// store player ally/neutral/enemy data as arrays
var allies = [];
var mutualAllies = [];
var neutrals = [];
var enemies = [];
for (var j = 0; j < n; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
var playerData = {
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"colour": cmpPlayer.GetColour(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"heroes": cmpPlayer.GetHeroes(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits.GetLimits(),
"entityCounts": cmpPlayerEntityLimits.GetCounts(),
"entityLimitChangers": cmpPlayerEntityLimits.GetLimitChangers(),
"researchQueued": cmpTechnologyManager.GetQueuedResearch(),
"researchStarted": cmpTechnologyManager.GetStartedResearch(),
"researchedTechs": cmpTechnologyManager.GetResearchedTechs(),
"classCounts": cmpTechnologyManager.GetClassCounts(),
"typeCountsByClass": cmpTechnologyManager.GetTypeCountsByClass()
};
ret.players.push(playerData);
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
{
ret.circularMap = cmpRangeManager.GetLosCircular();
}
// Add timeElapsed
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
return ret;
};
GuiInterface.prototype.GetExtendedSimulationState = function(player)
{
// Get basic simulation info
var ret = this.GetSimulationState();
// Add statistics to each player
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var n = cmpPlayerMan.GetNumPlayers();
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
}
// Add bartering prices
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterPrices = cmpBarter.GetPrices();
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function(player)
{
this.renamedEntities = [];
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
var ret = {
"id": ent,
"template": template,
"alertRaiser": null,
"buildEntities": null,
"identity": null,
"foundation": null,
"garrisonHolder": null,
"gate": null,
"guard": null,
"pack": null,
"player": -1,
"position": null,
"production": null,
"rallyPoint": null,
"rotation": null,
"trader": null,
"unitAI": null,
"visibility": null,
};
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
{
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName()
};
}
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
ret.rotation = cmpPosition.GetRotation();
}
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = Math.ceil(cmpHealth.GetHitpoints());
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
ret.needsHeal = !cmpHealth.IsUnhealable();
}
var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
{
ret.buildEntities = cmpBuilder.GetEntitiesList();
}
var cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
{
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress(),
};
}
var cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"queue": cmpProductionQueue.GetQueue(),
};
}
var cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
{
ret.trader = {
"goods": cmpTrader.GetGoods(),
"requiredGoods": cmpTrader.GetRequiredGoods()
};
}
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
{
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
}
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
ret.player = cmpOwnership.GetOwner();
}
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
ret.rallyPoint = {'position': cmpRallyPoint.GetPositions()[0]}; // undefined or {x,z} object
}
var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
}
var 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(),
+ "possibleStances": cmpUnitAI.GetPossibleStances(),
};
// Add some information needed for ungarrisoning
if (cmpUnitAI.IsGarrisoned() && ret.player !== undefined)
ret.template = "p" + ret.player + "&" + ret.template;
}
var cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
{
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
}
var cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
{
ret.gate = {
"locked": cmpGate.IsLocked(),
};
}
var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
{
ret.alertRaiser = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
return ret;
};
/**
* Get additionnal entity info, rarely used in the gui
*/
GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
{
var ret = {
"armour": null,
"attack": null,
"barterMarket": null,
"buildingAI": null,
"healer": null,
"obstruction": null,
"turretParent":null,
"promotion": null,
"resourceCarrying": null,
"resourceDropsite": null,
"resourceGatherRates": null,
"resourceSupply": null,
};
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all?
ret.attack = cmpAttack.GetAttackStrengths(type);
var range = cmpAttack.GetRange(type);
ret.attack.type = type;
ret.attack.minRange = range.min;
ret.attack.maxRange = range.max;
var timers = cmpAttack.GetTimers(type);
ret.attack.prepareTime = timers.prepare;
ret.attack.repeatTime = timers.repeat;
if (type == "Ranged")
{
ret.attack.elevationBonus = range.elevationBonus;
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the rage in front of it, no spread. So angle = 0
ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack.elevationAdaptedRange = ret.attack.maxRange;
}
}
else
{
// not a ranged attack, set some defaults
ret.attack.elevationBonus = 0;
ret.attack.elevationAdaptedRange = ret.attack.maxRange;
}
}
var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
{
ret.armour = cmpArmour.GetArmourStrengths();
}
var cmpAuras = Engine.QueryInterface(ent, IID_Auras)
if (cmpAuras)
{
ret.auras = cmpAuras.GetDescriptions();
}
var cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
{
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
}
var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
{
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
}
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
var cmpResourceSupply = Engine.QueryInterface(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(),
"gatherers": cmpResourceSupply.GetGatherers()
};
}
var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
}
var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes()
};
}
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
{
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
}
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
{
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterMarket = { "prices": cmpBarter.GetPrices() };
}
var cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
{
ret.healer = {
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
}
return ret;
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var rot = {x:0, y:0, z:0};
var pos = {x:cmd.x,z:cmd.z};
pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z);
var elevationBonus = cmd.elevationBonus || 0;
var range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, extendedName)
{
var name = extendedName;
// Special case for garrisoned units which have a extended template
if (extendedName.indexOf("&") != -1)
name = extendedName.slice(extendedName.indexOf("&")+1);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
var ret = {};
if (template.Armour)
{
ret.armour = {
"hack": ApplyValueModificationsToTemplate("Armour/Hack", +template.Armour.Hack, player, template),
"pierce": ApplyValueModificationsToTemplate("Armour/Pierce", +template.Armour.Pierce, player, template),
"crush": ApplyValueModificationsToTemplate("Armour/Crush", +template.Armour.Crush, player, template),
};
}
if (template.Attack)
{
ret.attack = {};
for (var type in template.Attack)
{
ret.attack[type] = {
"hack": ApplyValueModificationsToTemplate("Attack/"+type+"/Hack", +(template.Attack[type].Hack || 0), player, template),
"pierce": ApplyValueModificationsToTemplate("Attack/"+type+"/Pierce", +(template.Attack[type].Pierce || 0), player, template),
"crush": ApplyValueModificationsToTemplate("Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0), player, template),
"minRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0), player, template),
"maxRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange, player, template),
"elevationBonus": ApplyValueModificationsToTemplate("Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0), player, template),
};
}
}
if (template.Auras)
{
ret.auras = {};
for each (var aura in template.Auras)
if (aura.AuraName)
ret.auras[aura.AuraName] = aura.AuraDescription || null;
}
if (template.BuildRestrictions)
{
// required properties
ret.buildRestrictions = {
"placementType": template.BuildRestrictions.PlacementType,
"territory": template.BuildRestrictions.Territory,
"category": template.BuildRestrictions.Category,
};
// optional properties
if (template.BuildRestrictions.Distance)
{
ret.buildRestrictions.distance = {
"fromCategory": template.BuildRestrictions.Distance.FromCategory,
};
if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance;
if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance;
}
}
if (template.TrainingRestrictions)
{
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category,
};
}
if (template.Cost)
{
ret.cost = {};
if (template.Cost.Resources.food) ret.cost.food = ApplyValueModificationsToTemplate("Cost/Resources/food", +template.Cost.Resources.food, player, template);
if (template.Cost.Resources.wood) ret.cost.wood = ApplyValueModificationsToTemplate("Cost/Resources/wood", +template.Cost.Resources.wood, player, template);
if (template.Cost.Resources.stone) ret.cost.stone = ApplyValueModificationsToTemplate("Cost/Resources/stone", +template.Cost.Resources.stone, player, template);
if (template.Cost.Resources.metal) ret.cost.metal = ApplyValueModificationsToTemplate("Cost/Resources/metal", +template.Cost.Resources.metal, player, template);
if (template.Cost.Population) ret.cost.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, player, template);
if (template.Cost.PopulationBonus) ret.cost.populationBonus = ApplyValueModificationsToTemplate("Cost/PopulationBonus", +template.Cost.PopulationBonus, player, template);
if (template.Cost.BuildTime) ret.cost.time = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, player, template);
}
if (template.Footprint)
{
ret.footprint = {"height": template.Footprint.Height};
if (template.Footprint.Square)
ret.footprint.square = {"width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"]};
else if (template.Footprint.Circle)
ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]};
else
warn("[GetTemplateData] Unrecognized Footprint type");
}
if (template.Obstruction)
{
ret.obstruction = {
"active": ("" + template.Obstruction.Active == "true"),
"blockMovement": ("" + template.Obstruction.BlockMovement == "true"),
"blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"),
"blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"),
"blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"),
"disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"),
"disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"),
"shape": {}
};
if (template.Obstruction.Static)
{
ret.obstruction.shape.type = "static";
ret.obstruction.shape.width = +template.Obstruction.Static["@width"];
ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"];
}
else if (template.Obstruction.Unit)
{
ret.obstruction.shape.type = "unit";
ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"];
}
else
{
ret.obstruction.shape.type = "cluster";
}
}
if (template.Pack)
{
ret.pack = {
"state": template.Pack.State,
"time": ApplyValueModificationsToTemplate("Pack/Time", +template.Pack.Time, player, template),
};
}
if (template.Health)
{
ret.health = Math.round(ApplyValueModificationsToTemplate("Health/Max", +template.Health.Max, player, template));
}
if (template.Identity)
{
ret.selectionGroupName = template.Identity.SelectionGroupName;
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
ret.gateConversionTooltip = template.Identity.GateConversionTooltip;
ret.requiredTechnology = template.Identity.RequiredTechnology;
ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity);
}
if (template.UnitMotion)
{
ret.speed = {
"walk": ApplyValueModificationsToTemplate("UnitMotion/WalkSpeed", +template.UnitMotion.WalkSpeed, player, template),
};
if (template.UnitMotion.Run) ret.speed.run = ApplyValueModificationsToTemplate("UnitMotion/Run/Speed", +template.UnitMotion.Run.Speed, player, template);
}
if (template.Trader)
ret.trader = template.Trader;
if (template.WallSet)
{
ret.wallSet = {
"templates": {
"tower": template.WallSet.Templates.Tower,
"gate": template.WallSet.Templates.Gate,
"long": template.WallSet.Templates.WallLong,
"medium": template.WallSet.Templates.WallMedium,
"short": template.WallSet.Templates.WallShort,
},
"maxTowerOverlap": +template.WallSet.MaxTowerOverlap,
"minTowerOverlap": +template.WallSet.MinTowerOverlap,
};
}
if (template.WallPiece)
{
ret.wallPiece = {"length": +template.WallPiece.Length};
}
return ret;
};
GuiInterface.prototype.GetTechnologyData = function(player, name)
{
var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
var template = cmpTechTempMan.GetTemplate(name);
if (!template)
{
warn("Tried to get data for invalid technology: " + name);
return null;
}
var ret = {};
// Get specific name for this civ or else the generic specific name
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var specific = undefined;
if (template.specificName)
{
if (template.specificName[cmpPlayer.GetCiv()])
specific = template.specificName[cmpPlayer.GetCiv()];
else
specific = template.specificName['generic'];
}
ret.name = {
"specific": specific,
"generic": template.genericName,
};
if (template.icon)
ret.icon = "technologies/" + template.icon;
else
ret.icon = null;
ret.cost = {
"food": template.cost ? (+template.cost.food) : 0,
"wood": template.cost ? (+template.cost.wood) : 0,
"metal": template.cost ? (+template.cost.metal) : 0,
"stone": template.cost ? (+template.cost.stone) : 0,
"time": template.researchTime ? (+template.researchTime) : 0,
}
ret.tooltip = template.tooltip;
if (template.requirementsTooltip)
ret.requirementsTooltip = template.requirementsTooltip;
else
ret.requirementsTooltip = "";
if (template.requirements.class)
ret.classRequirements = {"class": template.requirements.class, "number": template.requirements.number};
ret.description = template.description;
return ret;
};
GuiInterface.prototype.IsTechnologyResearched = function(player, tech)
{
if (!tech)
return true;
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, tech)
{
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(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)
{
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
var ret = {};
for (var tech in cmpTechnologyManager.GetTechsStarted())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
var cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
else
ret[tech].progress = 0;
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player)
{
var cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
var cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
return cmpAttackDetection.GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, amounts)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
return cmpPlayer.GetNeededResources(amounts);
};
GuiInterface.prototype.AddTimeNotification = function(notification)
{
var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
notification.endTime = notification.duration + time;
notification.id = ++this.timeNotificationID;
this.timeNotifications.push(notification);
this.timeNotifications.sort(function (n1, n2){return n2.endTime - n1.endTime});
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
for (var i in this.timeNotifications)
{
if (this.timeNotifications[i].id == notificationID)
{
this.timeNotifications.splice(i);
return;
}
}
};
GuiInterface.prototype.GetTimeNotifications = function(playerID)
{
var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
var toDelete = [];
for (var n of this.timeNotifications)
{
n.time = n.endTime - time;
if (n.time < 0)
toDelete.push(n.id);
}
for (var id of toDelete)
this.DeleteTimeNotification(id);
return this.timeNotifications;
};
GuiInterface.prototype.PushNotification = function(notification)
{
this.notifications.push(notification);
};
GuiInterface.prototype.GetNextNotification = function()
{
if (this.notifications.length)
return this.notifications.pop();
else
return false;
};
GuiInterface.prototype.GetAvailableFormations = function(player, data)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player);
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)
{
var r = {};
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return r;
r.name = template.Formation.FormationName;
r.tooltip = template.Formation.DisabledTooltip || "";
r.icon = template.Formation.Icon;
return r;
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
for each (var ent in data.ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
// GetLastFormationName is named in a strange way as it (also) is
// the value of the current formation (see Formation.js LoadFormation)
if (cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
return true;
}
}
return false;
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for each (var ent in data.ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmpUnitAI.GetStanceName() == data.stance)
return true;
}
}
return false;
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var playerColours = {}; // cache of owner -> colour map
for each (var ent in cmd.entities)
{
var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's colour:
var owner = -1;
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
var colour = playerColours[owner];
if (!colour)
{
colour = {"r":1, "g":1, "b":1};
var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
if (cmpPlayer)
colour = cmpPlayer.GetColour();
playerColours[owner] = colour;
}
cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha}, cmd.selected);
}
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
for each (var ent in cmd.entities)
{
var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.SetEnabled(cmd.enabled);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player);
};
/**
* 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)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player);
// If there are some rally points already displayed, first hide them
for each (var ent in this.entsRallyPointsDisplayed)
{
var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities
for each (var ent in cmd.entities)
{
var 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)
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
var 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
var pos;
if (cmd.x && cmd.z)
pos = cmd;
else
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for each (var posi in cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({'x': posi.x, 'y': posi.z});
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
/**
* 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
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
var result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
}
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
{
this.placementEntity = undefined;
}
else
{
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
}
if (this.placementEntity)
{
var ent = this.placementEntity[1];
// Move the preview into the right location
var pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
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)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
// Set it to a red shade if this is an invalid location
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColour(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': ...,
* 'populationBonus': ...,
* }
* }
*
* @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)
{
var wallSet = cmd.wallSet;
var start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
var end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// 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
var numCleared = 0;
for (var tpl in this.placementWallEntities)
{
for each (var ent in this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
else
{
// Move all existing cached entities outside of the world and reset their use count
for (var tpl in this.placementWallEntities)
{
for each (var ent in this.placementWallEntities[tpl].entities)
{
var 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 each (var tpl in wallSet.templates)
{
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, tpl),
};
// ensure that the loaded template data contains a wallPiece component
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)
{
var snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
var 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)
{
var 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
var result = {
"pieces": [],
"cost": {"food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0},
};
var previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// 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)
{
var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && 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
var startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
var 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 > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
var 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
var endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
var 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 > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
});
}
}
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
var 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.
var allPiecesValid = true;
var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (var i = 0; i < previewEntities.length; ++i)
{
var entInfo = previewEntities[i];
var ent = null;
var tpl = entInfo.template;
var tplData = this.placementWallEntities[tpl].templateData;
var entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
{
// reuse an existing one
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
var 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)
{
var terrainGroundPrev = null;
var 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)
{
var targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
var 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.
var primaryControlGroup = ent;
var 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);
// check whether this wall piece can be validly positioned here
var validPlacement = false;
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// tell GetLosVisibility to force RetainInFog because preview entities set this to false,
// which would show them as hidden instead of fogged
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
var visible = (cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
if (visible)
{
var 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.
result.cost.food += tplData.cost.food;
result.cost.wood += tplData.cost.wood;
result.cost.stone += tplData.cost.stone;
result.cost.metal += tplData.cost.metal;
result.cost.population += tplData.cost.population;
result.cost.populationBonus += tplData.cost.populationBonus;
result.cost.time += tplData.cost.time;
}
var canAfford = true;
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
var canAfford = false;
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColour(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)
{
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.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)
var minDist2 = -1;
var minDistEntitySnapData = null;
var radius2 = data.snapRadius * data.snapRadius;
for each (var ent in data.snapEntities)
{
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
var pos = cmpPosition.GetPosition();
var 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 (template.BuildRestrictions.Category == "Dock")
{
var angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {"x": data.x, "z": data.z, "angle": angle};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
// Ignore if no entity was passed
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerEntities = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) {
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return false;
var cmpIdentity = Engine.QueryInterface(e, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass(data.idleClass))
return false;
return true;
});
var idleUnits = [];
for (var j = 0; j < playerEntities.length; ++j)
{
var ent = playerEntities[j];
if (ent <= data.prevUnit|0 || data.excludeUnits.indexOf(ent) > -1)
continue;
idleUnits.push(ent);
playerEntities.splice(j--, 1);
if (data.limit && idleUnits.length >= data.limit)
break;
}
return idleUnits;
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
var cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
var firstMarket = cmpEntityTrader.GetFirstMarket();
var secondMarket = cmpEntityTrader.GetSecondMarket();
var result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGain();
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGain(),
};
}
else if (!firstMarket)
{
result = {"type": "set first"};
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = {"type": "set first"};
}
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
if (!cmpAttack)
return false;
return cmpAttack.CanAttack(data.target);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
var cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
cmpPathfinder.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
cmpObstructionManager.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for each (var ent in data.entities)
{
var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var traders = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) {
return Engine.QueryInterface(e, IID_Trader);
});
var landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
var shipTrader = { "total": 0, "trading": 0 };
for each (var ent in traders)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
var 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")
{
var holder = cmpUnitAI.order.data.target;
var 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, tradingGoods)
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
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.)
var exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetExtendedEntityState": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNextNotification": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"SetSelectionHighlight": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"DisplayRallyPoint": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 15374)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 15375)
@@ -1,5809 +1,5814 @@
function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player." +
"" +
"" +
"" +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"standground" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"skittish" +
"domestic" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"" +
"";
// Unit stances.
// There some targeting options:
// targetVisibleEnemies: anything in vision range is a viable target
// targetAttackersAlways: anything that hurts us is a viable target,
// possibly overriding user orders!
// targetAttackersPassive: anything that hurts us is a viable target,
// if we're on a passive/unforced order (e.g. gathering/building)
// There are some response options, triggered when targets are detected:
// respondFlee: run away
// respondChase: start chasing after the enemy
// respondChaseBeyondVision: start chasing, and don't stop even if it's out
// of this unit's vision range (though still visible to the player)
// respondStandGround: attack enemy but don't move at all
// respondHoldGround: attack enemy but don't move far from current position
// TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
// do worry around armies slaughtering the guy standing next to you), etc.
var g_Stances = {
"violent": {
targetVisibleEnemies: true,
targetAttackersAlways: true,
targetAttackersPassive: true,
respondFlee: false,
respondChase: true,
respondChaseBeyondVision: true,
respondStandGround: false,
respondHoldGround: false,
},
"aggressive": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: true,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: false,
},
"defensive": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: true,
},
"passive": {
targetVisibleEnemies: false,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: true,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: false,
},
"standground": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: true,
respondHoldGround: false,
},
};
// See ../helpers/FSM.js for some documentation of this FSM specification syntax
UnitAI.prototype.UnitFsmSpec = {
// Default event handlers:
"MoveCompleted": function() {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"MoveStarted": function() {
// ignore spurious movement messages
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
"LosHealRangeUpdate": function(msg) {
// ignore newly-seen injured units by default
},
"Attacked": function(msg) {
// ignore attacker
},
"HealthChanged": function(msg) {
// ignore
},
"PackFinished": function(msg) {
// ignore
},
"PickupCanceled": function(msg) {
// ignore
},
"GuardedAttacked": function(msg) {
// ignore
},
// Formation handlers:
"FormationLeave": function(msg) {
// ignore when we're not in FORMATIONMEMBER
},
// Called when being told to walk as part of a formation
"Order.FormationWalk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);
this.SetNextStateAlwaysEntering("FORMATIONMEMBER.WALKING");
},
// Special orders:
// (these will be overridden by various states)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
// Individual orders:
// (these will switch the unit out of formation mode)
"Order.Stop": function(msg) {
// We have no control over non-domestic animals.
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
// Stop moving immediately.
this.StopMoving();
this.FinishOrder();
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
"Order.Walk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
if (!this.order.data.max)
this.MoveToPoint(this.order.data.x, this.order.data.z);
else
this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.WalkAndFight": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
this.MoveToPoint(this.order.data.x, this.order.data.z);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING"); // WalkAndFight not applicable for animals
else
this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING");
},
"Order.WalkToTarget": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
var ok = this.MoveToTarget(this.order.data.target);
if (ok)
{
// We've started walking to the given point
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
},
"Order.PickupUnit": function(msg) {
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return;
}
// Check if we need to move TODO implement a better way to know if we are on the shoreline
var needToMove = true;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x)
&& (this.lastShorelinePosition.z == cmpPosition.GetPosition().z))
{
// we were already on the shoreline, and have not moved since
if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50)
needToMove = false;
}
// TODO: what if the units are on a cliff ? the ship will go below the cliff
// and the units won't be able to garrison. Should go to the nearest (accessible) shore
if (needToMove && this.MoveToTarget(this.order.data.target))
{
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
}
},
"Order.Guard": function(msg) {
if (!this.AddGuard(this.order.data.target))
{
this.FinishOrder();
return;
}
if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
},
"Order.Flee": function(msg) {
// We use the distance between the entities to account for ranged attacks
var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1))
{
// We've started fleeing from the given target
if (this.IsAnimal())
this.SetNextState("ANIMAL.FLEEING");
else
this.SetNextState("INDIVIDUAL.FLEEING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
},
"Order.Attack": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Work out how to attack the given target
var type = this.GetBestAttackAgainst(this.order.data.target);
if (!type)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.order.data.attackType = type;
// If we are already at the target, try attacking it from here
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.StopMoving();
// For packable units within attack range:
// 1. If unpacked, we can attack the target.
// 2. If packed, we first need to unpack, then follow case 1.
if (this.CanUnpack())
{
// Ignore unforced attacks
// TODO: use special stances instead?
if (!this.order.data.force)
{
this.FinishOrder();
return;
}
// Case 2: unpack
this.PushOrderFront("Unpack", { "force": true });
return;
}
if (this.order.data.attackType == this.oldAttackType)
{
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
}
else
{
if (this.IsAnimal())
this.SetNextStateAlwaysEntering("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextStateAlwaysEntering("INDIVIDUAL.COMBAT.ATTACKING");
}
return;
}
// For packable units out of attack range:
// 1. If packed, we need to move to attack range and then unpack.
// 2. If unpacked, we first need to pack, then follow case 1.
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack)
{
// Ignore unforced attacks
// TODO: use special stances instead?
if (!this.order.data.force)
{
this.FinishOrder();
return;
}
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
}
// If we can't reach the target, but are standing ground, then abandon this attack order.
// Unless we're hunting, that's a special case where we should continue attacking our target.
if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret())
{
this.FinishOrder();
return;
}
// Try to move within attack range
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// We've started walking to the given point
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this attack order
this.FinishOrder();
},
"Order.Heal": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Healers can't heal themselves.
if (this.order.data.target == this.entity)
{
this.FinishOrder();
return;
}
// Check if the target is in range
if (this.CheckTargetRange(this.order.data.target, IID_Heal))
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.HEAL.HEALING");
return;
}
// If we can't reach the target, but are standing ground,
// then abandon this heal order
if (this.GetStance().respondStandGround && !this.order.data.force)
{
this.FinishOrder();
return;
}
// Try to move within heal range
if (this.MoveToTargetRange(this.order.data.target, IID_Heal))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this heal order
this.FinishOrder();
},
"Order.Gather": function(msg) {
// If the target is still alive, we need to kill it first
if (this.MustKillGatherTarget(this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
if (!this.GetBestAttackAgainst(this.order.data.target))
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
this.FinishOrder();
return;
}
// The target was visible when this order was issued,
// but could now be invisible again.
if (!this.CheckTargetVisible(this.order.data.target))
{
if (this.order.data.secondTry === undefined)
{
this.order.data.secondTry = true;
this.PushOrderFront("Walk", this.order.data.lastPos);
}
else
{
// We couldn't move there, or the target moved away
this.FinishOrder();
}
return;
}
this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true });
return;
}
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING");
}
},
"Order.GatherNearPosition": function(msg) {
// Move the unit to the position to gather from.
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("INDIVIDUAL.GATHER.WALKING");
},
"Order.ReturnResource": function(msg) {
// Check if the dropsite is already in range
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes);
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
// Try to move to the dropsite
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
{
// We've started walking to the target
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
return;
}
// Oops, we can't reach the dropsite.
// Maybe we should try to pick another dropsite, to find an
// accessible one?
// For now, just give up.
this.StopMoving();
this.FinishOrder();
return;
},
"Order.Trade": function(msg) {
// We must check if this trader has both markets in case it was a back-to-work order
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || ! cmpTrader.HasBothMarkets())
{
this.FinishOrder();
return;
}
var nextMarket = cmpTrader.GetNextMarket();
if (nextMarket == this.order.data.firstMarket)
var state = "TRADE.APPROACHINGFIRSTMARKET";
else
var state = "TRADE.APPROACHINGSECONDMARKET";
// TODO find the nearest way-point from our position, and start with it
this.waypoints = undefined;
if (this.MoveToMarket(nextMarket))
{
// We've started walking to the next market
this.SetNextState(state);
}
else
this.FinishOrder();
},
"Order.Repair": function(msg) {
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
}
},
"Order.Garrison": function(msg) {
if (this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move to the garrison target.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
// Case 2: pack
this.PushOrderFront("Pack", { "force": true });
return;
}
if (this.MoveToGarrisonRange(this.order.data.target))
{
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
}
else
{
// We do a range check before actually garrisoning
this.StopMoving();
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
}
},
"Order.Autogarrison": function(msg) {
this.SetNextState("INDIVIDUAL.AUTOGARRISON");
},
"Order.Alert": function(msg) {
this.alertRaiser = this.order.data.raiser;
// Find a target to garrison into, if we don't already have one
if (!this.alertGarrisoningTarget)
this.alertGarrisoningTarget = this.FindNearbyGarrisonHolder();
if (this.alertGarrisoningTarget)
this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget});
else
this.FinishOrder();
},
"Order.Cheering": function(msg) {
this.SetNextState("INDIVIDUAL.CHEERING");
},
"Order.Pack": function(msg) {
if (this.CanPack())
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.PACKING");
}
},
"Order.Unpack": function(msg) {
if (this.CanUnpack())
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.UNPACKING");
}
},
"Order.CancelPack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
"Order.CancelUnpack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKING");
},
"Order.WalkAndFight": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKINGANDFIGHTING");
},
"Order.MoveIntoFormation": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("FORMING");
},
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToTarget": function(msg) {
if (this.MoveToTarget(this.order.data.target))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToPointRange": function(msg) {
if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.Guard": function(msg) {
this.CallMemberFunction("Guard", [msg.data.target, false]);
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.Disband();
},
"Order.Stop": function(msg) {
this.CallMemberFunction("Stop", [false]);
this.FinishOrder();
},
"Order.Attack": function(msg) {
var target = msg.data.target;
var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
if (this.MoveToTargetAttackRange(target, target))
{
this.SetNextState("COMBAT.APPROACHING");
return;
}
}
this.FinishOrder();
return;
}
this.CallMemberFunction("Attack", [target, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
{
this.FinishOrder();
return;
}
// Check if we are already in range, otherwise walk there
if (!this.CheckGarrisonRange(msg.data.target))
{
if (!this.CheckTargetVisible(msg.data.target))
{
this.FinishOrder();
return;
}
else
{
// Out of range; move there in formation
if (this.MoveToGarrisonRange(msg.data.target))
{
this.SetNextState("GARRISON.APPROACHING");
return;
}
}
}
this.SetNextState("GARRISON.GARRISONING");
},
"Order.Gather": function(msg) {
if (this.MustKillGatherTarget(msg.data.target))
{
// The target was visible when this order was given,
// but could now be invisible.
if (!this.CheckTargetVisible(msg.data.target))
{
if (msg.data.secondTry === undefined)
{
msg.data.secondTry = true;
this.PushOrderFront("Walk", msg.data.lastPos);
}
else
{
// We couldn't move there, or the target moved away
this.FinishOrder();
}
return;
}
this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true });
return;
}
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target isn't gatherable or not visible any more.
this.FinishOrder();
// TODO: Should we issue a gather-near-position order
// if the target isn't gatherable/doesn't exist anymore?
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Gather", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.GatherNearPosition": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
{
// Out of range; move there in formation
this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
return;
}
this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Heal": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Heal", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Repair": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The building was finished or destroyed
this.FinishOrder();
else
// Out of range move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.ReturnResource": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Pack": function(msg) {
this.CallMemberFunction("Pack", [false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"Order.Unpack": function(msg) {
this.CallMemberFunction("Unpack", [false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
"IDLE": {
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
},
},
"WALKING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
this.CallMemberFunction("ResetFinishOrder", []);
},
},
"WALKINGANDFIGHTING": {
"enter": function(msg) {
this.StartTimer(0, 1000);
},
"Timer": function(msg) {
// check if there are no enemies to attack
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopTimer();
},
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
this.CallMemberFunction("ResetFinishOrder", []);
},
},
"GARRISON":{
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
this.SetNextState("GARRISONING");
},
},
"GARRISONING": {
"enter": function() {
// If a pickup has been requested, cancel it as it will be requested by members
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
this.CallMemberFunction("Garrison", [this.order.data.target, false]);
this.SetNextStateAlwaysEntering("MEMBER");
},
},
},
"FORMING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, false);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
{
this.CallMemberFunction("ResetFinishOrder", []);
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.FindInPosition();
}
},
"COMBAT": {
"APPROACHING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"MoveCompleted": function(msg) {
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.CallMemberFunction("Attack", [this.order.data.target, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
},
"ATTACKING": {
// Wait for individual members to finish
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(false, false);
this.StartTimer(200, 200);
var target = this.order.data.target;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(target);
this.PushOrderFront("WalkToTargetRange", { "target": target, "min": range.min, "max": range.max });
return;
}
this.FinishOrder();
return;
}
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(target);
this.FinishOrder();
this.PushOrderFront("Attack", { "target": target, "force": false });
return;
}
this.FinishOrder();
return;
}
},
"leave": function(msg) {
this.StopTimer();
},
},
},
"MEMBER": {
// Wait for individual members to finish
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
this.StartTimer(1000, 1000);
},
"Timer": function(msg) {
// Have all members finished the task?
if (!this.TestAllMemberFunction("HasFinishedOrder", []))
return;
this.CallMemberFunction("ResetFinishOrder", []);
// Execute the next order
if (this.FinishOrder())
{
// if WalkAndFight order, look for new target before moving again
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
},
"leave": function(msg) {
this.StopTimer();
},
},
},
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
// We're not in a formation anymore, so no need to track this.
this.finishedOrder = false;
// Stop moving as soon as the formation disbands
this.StopMoving();
// If the controller handled an order but some members rejected it,
// they will have no orders and be in the FORMATIONMEMBER.IDLE state.
if (this.orderQueue.length)
{
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
return;
}
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
// Override the LeaveFoundation order since we're not doing
// anything more important (and we might be stuck in the WALKING
// state forever and need to get out of foundations in that case)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("WALKINGTOPOINT");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
"IDLE": {
"enter": function() {
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
return true;
},
},
"WALKING": {
"enter": function () {
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpFormation && cmpVisual)
{
cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk"));
cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run"));
}
this.SelectAnimation("move");
},
// Occurs when the unit has reached its destination and the controller
// is done moving. The controller is notified.
"MoveCompleted": function(msg) {
// We can only finish this order if the move was really completed.
if (!msg.data.error && this.FinishOrder())
return;
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
cmpVisual.ResetMoveAnimation("walk");
cmpVisual.ResetMoveAnimation("run");
}
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.SetInPosition(this.entity);
},
},
// Special case used by Order.LeaveFoundation
"WALKINGTOPOINT": {
"enter": function() {
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.UnsetInPosition(this.entity);
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.FinishOrder();
},
},
},
// States for entities not part of a formation:
"INDIVIDUAL": {
"enter": function() {
// Sanity-checking
if (this.IsAnimal())
error("Animal got moved into INDIVIDUAL.* state");
},
"Attacked": function(msg) {
// Respond to attack if we always target attackers, or if we target attackers
// during passive orders (e.g. gathering/repairing are never forced)
if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (!this.order || !this.order.data || !this.order.data.force)))
{
this.RespondToTargetedEntities([msg.data.attacker]);
}
},
"GuardedAttacked": function(msg) {
// do nothing if we have a forced order in queue before the guard order
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type == "Guard")
break;
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
return;
}
// if we already are targeting another unit still alive, finish with it first
if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack"))
if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker))
return;
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpIdentity && cmpIdentity.HasClass("Support") &&
cmpHealth && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// if the attacker is a building and we can repair the guarded, repair it rather than attacking
var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI);
if (cmpBuildingAI && this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
{
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// target the unit
if (this.CheckTargetVisible(msg.data.attacker))
this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false });
else
{
var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
// if we already had a WalkAndFight, keep only the most recent one in case the target has moved
if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
this.orderQueue.splice(1, 1);
}
},
"IDLE": {
"enter": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
var animationName = "idle";
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
animationName = cmpFormation.GetFormationAnimation(this.entity, animationName);
}
this.SelectAnimation(animationName);
// If the unit is guarding/escorting, go back to its duty
if (this.isGuardOf)
{
this.Guard(this.isGuardOf, false);
return true;
}
// The GUI and AI want to know when a unit is idle, but we don't
// want to send frequent spurious messages if the unit's only
// idle for an instant and will quickly go off and do something else.
// So we'll set a timer here and only report the idle event if we
// remain idle
this.StartTimer(1000);
// If a unit can heal and attack we first want to heal wounded units,
// so check if we are a healer and find whether there's anybody nearby to heal.
// (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
// If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
if (this.IsHealer() && this.FindNewHealTargets())
return true; // (abort the FSM transition since we may have already switched state)
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
if (this.FindNewTargets())
return true; // (abort the FSM transition since we may have already switched state)
// Nobody to attack - stay in idle
return false;
},
"leave": function() {
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
rangeMan.DisableActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
rangeMan.DisableActiveQuery(this.losHealRangeQuery);
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
"LosRangeUpdate": function(msg) {
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackEntitiesByPreference(msg.data.added);
}
},
"LosHealRangeUpdate": function(msg) {
this.RespondToHealableEntities(msg.data.added);
},
"Timer": function(msg) {
if (!this.isIdle)
{
this.isIdle = true;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
},
"WALKING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.FinishOrder();
},
},
"WALKINGANDFIGHTING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.StartTimer(0, 1000);
this.SelectAnimation("move");
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopTimer();
},
"MoveCompleted": function() {
this.FinishOrder();
},
},
"GUARD": {
"RemoveGuard": function() {
this.StopMoving();
this.FinishOrder();
},
"ESCORTING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.StartTimer(0, 1000);
this.SelectAnimation("move");
this.SetHeldPositionOnEntity(this.isGuardOf);
return false;
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.StopMoving();
this.FinishOrder();
return;
}
this.SetHeldPositionOnEntity(this.isGuardOf);
},
"leave": function(msg) {
this.SetMoveSpeed(this.GetWalkSpeed());
this.StopTimer();
},
"MoveStarted": function(msg) {
// Adapt the speed to the one of the target if needed
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange))
{
var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI);
if (cmpUnitAI)
{
var speed = cmpUnitAI.GetWalkSpeed();
if (speed < this.GetWalkSpeed())
this.SetMoveSpeed(speed);
}
}
},
"MoveCompleted": function() {
this.SetMoveSpeed(this.GetWalkSpeed());
if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("GUARDING");
},
},
"GUARDING": {
"enter": function () {
this.StartTimer(1000, 1000);
this.SetHeldPositionOnEntity(this.entity);
this.SelectAnimation("idle");
return false;
},
"LosRangeUpdate": function(msg) {
// Start attacking one of the newly-seen enemy (if any)
if (this.GetStance().targetVisibleEnemies)
this.AttackEntitiesByPreference(msg.data.added);
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.FinishOrder();
return;
}
// then check is the target has moved
if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("ESCORTING");
else
{
// if nothing better to do, check if the guarded needs to be healed or repaired
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpHealth && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()))
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
}
}
},
"leave": function(msg) {
this.StopTimer();
},
},
},
"FLEEING": {
"enter": function() {
this.PlaySound("panic");
// Run quickly
var speed = this.GetRunSpeed();
this.SelectAnimation("move");
this.SetMoveSpeed(speed);
},
"HealthChanged": function() {
var speed = this.GetRunSpeed();
this.SetMoveSpeed(speed);
},
"leave": function() {
// Reset normal speed
this.SetMoveSpeed(this.GetWalkSpeed());
},
"MoveCompleted": function() {
// When we've run far enough, stop fleeing
this.FinishOrder();
},
// TODO: what if we run into more enemies while fleeing?
},
"COMBAT": {
"Order.LeaveFoundation": function(msg) {
// Ignore the order as we're busy.
return { "discardOrder": true };
},
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else
// who's attacking us
},
"APPROACHING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
// Show carried resources when walking.
this.SetGathererAnimationOverride();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// If the unit needs to unpack, do so
if (this.CanUnpack())
this.SetNextState("UNPACKING");
else
this.SetNextState("ATTACKING");
}
else
{
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.SetNextState("APPROACHING");
}
else
{
// Give up
this.FinishOrder();
}
}
},
"Attacked": function(msg) {
// If we're attacked by a close enemy, we should try to defend ourself
// but only if we're not forced to target something else
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force)))
{
this.RespondToTargetedEntities([msg.data.attacker]);
}
},
},
"UNPACKING": {
"enter": function() {
// If we're not in range yet (maybe we stopped moving), move to target again
if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
this.SetNextState("APPROACHING");
else
{
// Give up
this.FinishOrder();
}
return true;
}
// In range, unpack
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Unpack();
return false;
},
"PackFinished": function(msg) {
this.SetNextState("ATTACKING");
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore further attacks while unpacking
},
},
"ATTACKING": {
"enter": function() {
var target = this.order.data.target;
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
// if the target is a formation, save the attacking formation, and pick a member
if (cmpFormation)
{
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity);
this.order.data.target = target;
}
// Check the target is still alive and attackable
if
(
this.TargetIsAlive(target) &&
this.CanAttack(target, this.order.data.forceResponse || null) &&
!this.CheckTargetAttackRange(target, this.order.data.attackType)
)
{
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetAttackRange(target, this.order.data.attackType))
{
this.SetNextState("COMBAT.CHASING");
return;
}
}
}
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType);
// If the repeat time since the last attack hasn't elapsed,
// delay this attack to avoid attacking too fast.
var prepare = this.attackTimers.prepare;
if (this.lastAttacked)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
// add prefix + no capital first letter for attackType
var animationName = "attack_" + this.order.data.attackType.toLowerCase();
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
animationName = cmpFormation.GetFormationAnimation(this.entity, animationName);
}
this.SelectAnimation(animationName, false, 1.0, "attack");
this.SetAnimationSync(prepare, this.attackTimers.repeat);
this.StartTimer(prepare, this.attackTimers.repeat);
// TODO: we should probably only bother syncing projectile attacks, not melee
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false;
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
// if the target is a formation, save the attacking formation, and pick a member
if (cmpFormation)
{
var thisObject = this;
var filter = function(t) {
return thisObject.TargetIsAlive(t) && thisObject.CanAttack(t, thisObject.order.data.forceResponse || null);
};
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity, filter);
this.order.data.target = target;
}
// Check the target is still alive and attackable
if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null))
{
// If we are hunting, first update the target position of the gather order so we know where will be the killed animal
if (this.order.data.hunting && this.orderQueue[1] && this.orderQueue[1].data.lastPos)
{
var cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
// Store the initial position, so that we can find the rest of the herd later
if (!this.orderQueue[1].data.initPos)
this.orderQueue[1].data.initPos = this.orderQueue[1].data.lastPos;
this.orderQueue[1].data.lastPos = cmpPosition.GetPosition();
// We still know where the animal is, so we shouldn't give up before going there
this.orderQueue[1].data.secondTry = undefined;
}
}
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
cmpAttack.PerformAttack(this.order.data.attackType, target);
// Check we can still reach the target for the next attack
if (this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.resyncAnimation)
{
this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType))
{
this.SetNextState("COMBAT.CHASING");
return;
}
}
}
// if we're targetting a formation, find a new member of that formation
var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
// if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search
if (target && cmpTargetFormation)
{
this.order.data.target = this.order.data.formationTarget;
this.TimerHandler(msg.data, msg.lateness);
return;
}
this.oldAttackType = this.order.data.attackType;
// Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
// Except if in WalkAndFight mode where we look for more ennemies around before moving again
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
// See if we can switch to a new nearby enemy
if (this.FindNewTargets())
{
// Attempt to immediately re-enter the timer function, to avoid wasting the attack.
if (this.orderQueue.length > 0 && this.orderQueue[0].data.attackType == this.oldAttackType)
this.TimerHandler(msg.data, msg.lateness);
return;
}
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
// TODO: respond to target deaths immediately, rather than waiting
// until the next Timer event
"Attacked": function(msg) {
if (this.order.data.target != msg.data.attacker)
{
// If we're attacked by a close enemy, stronger than our current target,
// we choose to attack it, but only if we're not forced to target something else
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force)))
{
var ents = [this.order.data.target, msg.data.attacker];
SortEntitiesByPriority(ents);
if (ents[0] != this.order.data.target)
{
this.RespondToTargetedEntities(ents);
}
}
}
},
},
"CHASING": {
"enter": function () {
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.SelectAnimation("move");
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsFleeing())
{
// Run after a fleeing target
var speed = this.GetRunSpeed();
this.SetMoveSpeed(speed);
}
this.StartTimer(1000, 1000);
},
"HealthChanged": function() {
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsFleeing())
return;
var speed = this.GetRunSpeed();
this.SetMoveSpeed(speed);
},
"leave": function() {
// Reset normal speed in case it was changed
this.SetMoveSpeed(this.GetWalkSpeed());
// Show carried resources when walking.
this.SetGathererAnimationOverride();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
this.SetNextState("ATTACKING");
},
},
},
"GATHER": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("move");
this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
// check that we can gather from the resource we're supposed to gather from.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
{
// Save the current order's data in case we need it later
var oldType = this.order.data.type;
var oldTarget = this.order.data.target;
var oldTemplate = this.order.data.template;
// Try the next queued order if there is any
if (this.FinishOrder())
return true;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
ent != oldTarget
&& ((type.generic == "treasure" && oldType.generic == "treasure")
|| (type.specific == oldType.specific
&& (type.specific != "meat" || oldTemplate == template)))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return true;
}
else
{
// It's probably better in this case, to avoid units getting stuck around a dropsite
// in a "Target is far away, full, nearby are no good resources, return to dropsite" loop
// to order it to GatherNear the resource position.
var cmpPosition = Engine.QueryInterface(this.gatheringTarget, IID_Position);
if (cmpPosition)
{
var pos = cmpPosition.GetPosition();
this.GatherNearPosition(pos.x, pos.z, oldType, oldTemplate);
return true;
} else {
// we're kind of stuck here. Return resource.
var nearby = this.FindNearestDropsite(oldType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return true;
}
}
}
return true;
}
return false;
},
"MoveCompleted": function(msg) {
if (msg.data.error)
{
// We failed to reach the target
// remove us from the list of entities gathering from Resource.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply && cmpOwnership)
cmpSupply.RemoveGatherer(this.entity, cmpOwnership.GetOwner());
else if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
// Save the current order's data in case we need it later
var oldType = this.order.data.type;
var oldTarget = this.order.data.target;
var oldTemplate = this.order.data.template;
// Try the next queued order if there is any
if (this.FinishOrder())
return;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
ent != oldTarget
&& ((type.generic == "treasure" && oldType.generic == "treasure")
|| (type.specific == oldType.specific
&& (type.specific != "meat" || oldTemplate == template)))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Couldn't find anything else. Just try this one again,
// maybe we'll succeed next time
this.PerformGather(oldTarget, false, false);
return;
}
// We reached the target - start gathering from it now
this.SetNextState("GATHERING");
},
"leave": function() {
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
},
},
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function(msg) {
var resourceType = this.order.data.type;
var resourceTemplate = this.order.data.template;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
// If there is a nearby resource start gathering
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Couldn't find nearby resources, so give up
if (this.FinishOrder())
return;
// Nothing better to do: go back to dropsite
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// No dropsites, just give up
},
},
"GATHERING": {
"enter": function() {
this.gatheringTarget = this.order.data.target; // deleted in "leave".
// Check if the resource is full.
if (this.gatheringTarget)
{
// Check that we can gather from the resource we're supposed to gather from.
// Will only be added if we're not already in.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
{
this.gatheringTarget = INVALID_ENTITY;
this.StartTimer(0);
return false;
}
}
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
this.order.data.autoharvest = true;
// Calculate timing based on gather rates
// This allows the gather rate to control how often we gather, instead of how much.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget);
if (!rate)
{
// Try to find another target if the current one stopped existing
if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity))
{
// Let the Timer logic handle this
this.StartTimer(0);
return false;
}
// No rate, give up on gathering
this.FinishOrder();
return true;
}
// Scale timing interval based on rate, and start timer
// The offset should be at least as long as the repeat time so we use the same value for both.
var offset = 1000/rate;
var repeat = offset;
this.StartTimer(offset, repeat);
// We want to start the gather animation as soon as possible,
// but only if we're actually at the target and it's still alive
// (else it'll look like we're chopping empty air).
// (If it's not alive, the Timer handler will deal with sending us
// off to a different target.)
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
var typename = "gather_" + this.order.data.type.specific;
this.SelectAnimation(typename, false, 1.0, typename);
}
return false;
},
"leave": function() {
this.StopTimer();
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
// Show the carried resource, if we've gathered anything.
this.SetGathererAnimationOverride();
},
"Timer": function(msg) {
var resourceTemplate = this.order.data.template;
var resourceType = this.order.data.type;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity))
{
// Check we can still reach and gather from the target
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget))
{
// Gather the resources:
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
return;
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
cmpResourceGatherer.DropResources();
// Collect from the target
var status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
// (Keep this Gather order on the stack so we'll
// continue gathering after returning)
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on gathering.
this.FinishOrder();
return;
}
// We can gather more from this target, do so in the next timer
if (!status.exhausted)
return;
}
else
{
// Try to follow the target
if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.SetNextState("APPROACHING");
return;
}
// Can't reach the target, or it doesn't exist any more
// We want to carry on gathering resources in the same area as
// the old one. So try to get close to the old resource's
// last known position
var maxRange = 8; // get close but not too close
if (this.order.data.lastPos &&
this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
0, maxRange))
{
this.SetNextState("APPROACHING");
return;
}
}
}
// We're already in range, can't get anywhere near it or the target is exhausted.
var herdPos = this.order.data.initPos;
// Give up on this order and try our next queued order
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// Try to find a new resource of the same specific type near our current position:
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// If hunting, try to go to the initial herd position to see if we are more lucky
if (herdPos)
this.GatherNearPosition(herdPos.x, herdPos.z, resourceType, resourceTemplate);
// Nothing else to gather - if we're carrying anything then we should
// drop it off, and if not then we might as well head to the dropsite
// anyway because that's a nice enough place to congregate and idle
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// No dropsites - just give up
},
},
},
"HEAL": {
"Attacked": function(msg) {
// If we stand ground we will rather die than flee
if (!this.GetStance().respondStandGround && !this.order.data.force)
this.Flee(msg.data.attacker, false);
},
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
this.SetNextState("HEALING");
},
},
"HEALING": {
"enter": function() {
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
this.healTimers = cmpHeal.GetTimers();
// If the repeat time since the last heal hasn't elapsed,
// delay the action to avoid healing too fast.
var prepare = this.healTimers.prepare;
if (this.lastHealed)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
this.SelectAnimation("heal", false, 1.0, "heal");
this.SetAnimationSync(prepare, this.healTimers.repeat);
this.StartTimer(prepare, this.healTimers.repeat);
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false;
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check the target is still alive and healable
if (this.TargetIsAlive(target) && this.CanHeal(target))
{
// Check if we can still reach the target
if (this.CheckTargetRange(target, IID_Heal))
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastHealed = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
cmpHeal.PerformHeal(target);
if (this.resyncAnimation)
{
this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, IID_Heal))
{
this.SetNextState("HEAL.CHASING");
return;
}
}
}
// Can't reach it, healed to max hp or doesn't exist any more - give up
if (this.FinishOrder())
return;
// Heal another one
if (this.FindNewHealTargets())
return;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
},
"CHASING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function () {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function () {
this.SetNextState("HEALING");
},
},
},
// Returning to dropsite
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with the carry animation after stopping moving
this.SelectAnimation("idle");
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
cmpResourceGatherer.CommitResources(dropsiteTypes);
// Stop showing the carried resource animation.
this.SetGathererAnimationOverride();
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
// The dropsite was destroyed, or we couldn't reach it, or ownership changed
// Look for a new one.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var genericType = cmpResourceGatherer.GetMainCarryingType();
var nearby = this.FindNearestDropsite(genericType);
if (nearby)
{
this.FinishOrder();
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on returning.
this.FinishOrder();
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
// TODO: Inform player
},
"APPROACHINGFIRSTMARKET": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.firstMarket))
this.stopTrading();
}
else
this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "APPROACHINGSECONDMARKET");
},
},
"APPROACHINGSECONDMARKET": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.secondMarket))
this.stopTrading();
}
else
this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "APPROACHINGFIRSTMARKET");
},
},
},
"REPAIR": {
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.SetNextState("REPAIRING");
},
},
"REPAIRING": {
"enter": function() {
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if (this.order.data.force)
this.order.data.autoharvest = true;
this.order.data.force = false;
this.repairTarget = this.order.data.target; // temporary, deleted in "leave".
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// Can't reach it, no longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return true;
}
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
{
if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
else
this.FinishOrder();
return true;
}
// Check if the target is still repairable
var cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints())
{
// The building was already finished/fully repaired before we arrived;
// let the ConstructionFinished handler handle this.
this.OnGlobalConstructionFinished({"entity": this.repairTarget, "newentity": this.repairTarget});
return true;
}
var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation);
if (cmpFoundation)
cmpFoundation.AddBuilder(this.entity);
this.SelectAnimation("build", false, 1.0, "build");
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation);
if (cmpFoundation)
cmpFoundation.RemoveBuilder(this.entity);
delete this.repairTarget;
this.StopTimer();
},
"Timer": function(msg) {
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// No longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return;
}
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
cmpBuilder.PerformBuilding(this.repairTarget);
// if the building is completed, the leave() function will be called
// by the ConstructionFinished message
// in that case, the repairTarget is deleted, and we can just return
if (!this.repairTarget)
return;
if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
else if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
this.FinishOrder(); //can't approach and isn't in reach
},
},
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
// Save the current order's data in case we need it later
var oldData = this.order.data;
// Save the current state so we can continue walking if necessary
// FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
// Idle animation while moving towards finished construction looks weird (ghosty).
var oldState = this.GetCurrentState();
// We finished building it.
// Switch to the next order (if any)
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// If autocontinue explicitly disabled (e.g. by AI) then
// do nothing automatically
if (!oldData.autocontinue)
return;
// If this building was e.g. a farm of ours, the entities that recieved
// the build command should start gathering from it
if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity))
{
this.PerformGather(msg.data.newentity, true, false);
return;
}
// If this building was e.g. a farmstead of ours, entities that received
// the build command should look for nearby resources to gather
if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false))
{
var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
var types = cmpResourceDropsite.GetTypes();
// TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
// may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (types.indexOf(type.generic) != -1);
});
if (nearby)
{
this.PerformGather(nearby, true, false);
return;
}
}
// Look for a nearby foundation to help with
var nearbyFoundation = this.FindNearbyFoundation();
if (nearbyFoundation)
{
this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true);
return;
}
// Unit was approaching and there's nothing to do now, so switch to walking
if (oldState === "INDIVIDUAL.REPAIR.APPROACHING")
{
// We're already walking to the given point, so add this as a order.
this.WalkToTarget(msg.data.newentity, true);
}
},
},
"GARRISON": {
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
if(this.IsUnderAlert())
{
// check that we can garrison in the building we're supposed to garrison in
var cmpGarrisonHolder = Engine.QueryInterface(this.alertGarrisoningTarget, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
// Try to find another nearby building
var nearby = this.FindNearbyGarrisonHolder();
if (nearby)
{
this.alertGarrisoningTarget = nearby;
this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget});
}
else
this.FinishOrder();
}
else
this.SetNextState("GARRISONED");
}
else
this.SetNextState("GARRISONED");
},
},
"GARRISONED": {
"enter": function() {
// Target is not handled the same way with Alert and direct garrisoning
if(this.order.data.target)
var target = this.order.data.target;
else
{
if(!this.alertGarrisoningTarget)
{
// We've been unable to find a target nearby, so give up
this.FinishOrder();
return true;
}
var target = this.alertGarrisoningTarget;
}
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
// Check that we can garrison here
if (this.CanGarrison(target))
{
// Check that we're in range of the garrison target
if (this.CheckGarrisonRange(target))
{
// Check that garrisoning succeeds
if (cmpGarrisonHolder.Garrison(this.entity))
{
this.isGarrisoned = true;
if (this.formationController)
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
// disable rearrange for this removal,
// but enable it again for the next
// move command
var rearrange = cmpFormation.rearrange;
cmpFormation.SetRearrange(false);
cmpFormation.RemoveMembers([this.entity]);
cmpFormation.SetRearrange(rearrange);
}
}
// Check if we are garrisoned in a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
cmpResourceGatherer.CommitResources(dropsiteTypes);
this.SetGathererAnimationOverride();
}
}
// If a pickup has been requested, remove it
if (this.pickup)
{
var cmpHolderPosition = Engine.QueryInterface(target, IID_Position);
var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderPosition)
cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition();
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
if (this.IsTurret())
this.SetNextState("IDLE");
return false;
}
}
else
{
// Unable to reach the target, try again (or follow if it is a moving target)
// except if the does not exits anymore or its orders have changed
if (this.pickup)
{
var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity))
{
this.FinishOrder();
return true;
}
}
if (this.MoveToTarget(target))
{
this.SetNextState("APPROACHING");
return false;
}
}
}
// Garrisoning failed for some reason, so finish the order
this.FinishOrder();
return true;
},
"Order.Ungarrison": function() {
if (this.FinishOrder())
return;
},
"leave": function() {
this.isGarrisoned = false;
}
},
},
"AUTOGARRISON": {
"enter": function() {
this.isGarrisoned = true;
return false;
},
"Order.Ungarrison": function() {
if (this.FinishOrder())
return;
},
"leave": function() {
this.isGarrisoned = false;
}
},
"CHEERING": {
"enter": function() {
// Unit is invulnerable while cheering
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
this.SelectAnimation("promotion");
this.StartTimer(2800, 2800);
return false;
},
"leave": function() {
this.StopTimer();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(false);
},
"Timer": function(msg) {
this.FinishOrder();
},
},
"PACKING": {
"enter": function() {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Pack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while packing
},
},
"UNPACKING": {
"enter": function() {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Unpack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while unpacking
},
},
"PICKUP": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.SetNextState("LOADING");
},
"PickupCanceled": function() {
this.StopMoving();
this.FinishOrder();
},
},
"LOADING": {
"enter": function() {
this.SelectAnimation("idle");
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return true;
}
return false;
},
"PickupCanceled": function() {
this.FinishOrder();
},
},
},
},
"ANIMAL": {
"Attacked": function(msg) {
if (this.template.NaturalBehaviour == "skittish" ||
this.template.NaturalBehaviour == "passive")
{
this.Flee(msg.data.attacker, false);
}
else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive")
{
if (this.CanAttack(msg.data.attacker))
this.Attack(msg.data.attacker, false);
}
else if (this.template.NaturalBehaviour == "domestic")
{
// Never flee, stop what we were doing
this.SetNextState("IDLE");
}
},
"Order.LeaveFoundation": function(msg) {
// Run away from the foundation
this.FinishOrder();
this.PushOrderFront("Flee", { "target": msg.data.target, "force": false });
},
"IDLE": {
// (We need an IDLE state so that FinishOrder works)
"enter": function() {
// Start feeding immediately
this.SetNextState("FEEDING");
return true;
},
},
"ROAMING": {
"enter": function() {
// Walk in a random direction
this.SelectAnimation("walk", false, this.GetWalkSpeed());
this.MoveRandomly(+this.template.RoamDistance);
// Set a random timer to switch to feeding state
this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
this.SetFacePointAfterMove(false);
},
"leave": function() {
this.StopTimer();
this.SetFacePointAfterMove(true);
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.IsDangerousAnimal())
{
this.AttackVisibleEntity(msg.data.added);
}
// TODO: if two units enter our range together, we'll attack the
// first and then the second won't trigger another LosRangeUpdate
// so we won't notice it. Probably we should do something with
// ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to
// find any units that are already in range.
},
"Timer": function(msg) {
this.SetNextState("FEEDING");
},
"MoveCompleted": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
"FEEDING": {
"enter": function() {
// Stop and eat for a while
this.SelectAnimation("feeding");
this.StopMoving();
this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
},
"leave": function() {
this.StopTimer();
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.template.NaturalBehaviour == "violent")
{
this.AttackVisibleEntity(msg.data.added);
}
},
"MoveCompleted": function() { },
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
"FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals
"COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals
"WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals
// only used for domestic animals
},
};
UnitAI.prototype.Init = function()
{
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isGarrisoned = false;
this.isIdle = false;
this.lastFormationTemplate = "";
this.finishedOrder = false; // used to find if all formation members finished the order
this.heldPosition = undefined;
// Queue of remembered works
this.workOrders = [];
this.isGuardOf = undefined;
// "Town Bell" behaviour
this.alertRaiser = undefined;
this.alertGarrisoningTarget = undefined;
// For preventing increased action rate due to Stop orders or target death.
this.lastAttacked = undefined;
this.lastHealed = undefined;
this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsTurret = function()
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY;
};
UnitAI.prototype.ReactsToAlert = function(level)
{
return this.template.AlertReactiveLevel <= level;
};
UnitAI.prototype.IsUnderAlert = function()
{
return this.alertRaiser != undefined;
};
UnitAI.prototype.ResetAlert = function()
{
this.alertGarrisoningTarget = undefined;
this.alertRaiser = undefined;
};
UnitAI.prototype.GetAlertRaiser = function()
{
return this.alertRaiser;
};
UnitAI.prototype.IsFormationController = function()
{
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsFormationMember = function()
{
return (this.formationController != INVALID_ENTITY);
};
UnitAI.prototype.HasFinishedOrder = function()
{
return this.finishedOrder;
};
UnitAI.prototype.ResetFinishOrder = function()
{
this.finishedOrder = false;
};
UnitAI.prototype.IsAnimal = function()
{
return (this.template.NaturalBehaviour ? true : false);
};
UnitAI.prototype.IsDangerousAnimal = function()
{
return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" ||
this.template.NaturalBehaviour == "aggressive"));
};
UnitAI.prototype.IsDomestic = function()
{
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (!cmpIdentity)
return false;
return cmpIdentity.HasClass("Domestic");
};
UnitAI.prototype.IsHealer = function()
{
return Engine.QueryInterface(this.entity, IID_Heal);
};
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
};
UnitAI.prototype.IsGarrisoned = function()
{
return this.isGarrisoned || this.IsTurret();
};
UnitAI.prototype.IsFleeing = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "FLEEING");
};
UnitAI.prototype.IsWalking = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "WALKING");
};
/**
* return true if in WalkAndFight looking for new targets
*/
UnitAI.prototype.IsWalkingAndFighting = function()
{
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
return (cmpUnitAI && cmpUnitAI.IsWalkingAndFighting());
}
return (this.orderQueue.length > 0 && this.orderQueue[0].type == "WalkAndFight");
};
UnitAI.prototype.OnCreate = function()
{
if (this.IsAnimal())
this.UnitFsm.Init(this, "ANIMAL.FEEDING");
else if (this.IsFormationController())
this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
else
this.UnitFsm.Init(this, "INDIVIDUAL.IDLE");
};
UnitAI.prototype.OnDiplomacyChanged = function(msg)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
this.SetupRangeQuery();
};
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
this.SetupRangeQueries();
// If the unit isn't being created or dying, reset stance and clear orders (if not garrisoned).
if (msg.to != -1 && msg.from != -1)
{
// Switch to a virgin state to let states execute their leave handlers.
var index = this.GetCurrentState().indexOf(".");
if (index != -1)
this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index));
this.SetStance(this.template.DefaultStance);
if(!this.isGarrisoned)
this.Stop(false);
}
};
UnitAI.prototype.OnDestroy = function()
{
// Switch to an empty state to let states execute their leave handlers.
this.UnitFsm.SwitchToNextState(this, "");
// Clean up range queries
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
rangeMan.DestroyActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
};
UnitAI.prototype.OnVisionRangeChanged = function(msg)
{
// Update range queries
if (this.entity == msg.entity)
this.SetupRangeQueries();
};
UnitAI.prototype.HasPickupOrder = function(entity)
{
for each (var order in this.orderQueue)
if (order.type == "PickupUnit" && order.data.target == entity)
return true;
return false;
};
UnitAI.prototype.OnPickupRequested = function(msg)
{
// First check if we already have such a request
if (this.HasPickupOrder(msg.entity))
return;
// Otherwise, insert the PickUp order after the last forced order
this.PushOrderAfterForced("PickupUnit", { "target": msg.entity });
};
UnitAI.prototype.OnPickupCanceled = function(msg)
{
var cmpUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI);
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type == "PickupUnit" && this.orderQueue[i].data.target == msg.entity)
{
if (i == 0)
this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg});
else
this.orderQueue.splice(i, 1);
break;
}
}
};
// Wrapper function that sets up the normal and healer range queries.
UnitAI.prototype.SetupRangeQueries = function()
{
this.SetupRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
}
// Set up a range query for all enemy and gaia units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (this.losRangeQuery)
{
rangeMan.DestroyActiveQuery(this.losRangeQuery);
this.losRangeQuery = undefined;
}
var players = [];
if (owner != -1)
{
// If unit not just killed, get enemy players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
var numPlayers = playerMan.GetNumPlayers();
for (var i = 0; i < numPlayers; ++i)
{
// Exclude allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
if (cmpPlayer.IsEnemy(i))
players.push(i);
}
}
var range = this.GetQueryRange(IID_Attack);
this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, rangeMan.GetEntityFlagMask("normal"));
rangeMan.EnableActiveQuery(this.losRangeQuery);
};
// Set up a range query for all own or ally units within LOS range
// which can be healed.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupHealRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (this.losHealRangeQuery)
rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
var players = [owner];
if (owner != -1)
{
// If unit not just killed, get ally players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
var numPlayers = playerMan.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{
// Exclude gaia and enemies
if (cmpPlayer.IsAlly(i))
players.push(i);
}
}
var range = this.GetQueryRange(IID_Heal);
this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, rangeMan.GetEntityFlagMask("injured"));
rangeMan.EnableActiveQuery(this.losHealRangeQuery);
};
//// FSM linkage functions ////
UnitAI.prototype.SetNextState = function(state)
{
this.UnitFsm.SetNextState(this, state);
};
// This will make sure that the state is always entered even if this means leaving it and reentering it
// This is so that a state can be reinitialized with new order data without having to switch to an intermediate state
UnitAI.prototype.SetNextStateAlwaysEntering = function(state)
{
this.UnitFsm.SetNextStateAlwaysEntering(this, state);
};
UnitAI.prototype.DeferMessage = function(msg)
{
this.UnitFsm.DeferMessage(this, msg);
};
UnitAI.prototype.GetCurrentState = function()
{
return this.UnitFsm.GetCurrentState(this);
};
UnitAI.prototype.FsmStateNameChanged = function(state)
{
Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state });
};
/**
* Call when the current order has been completed (or failed).
* Removes the current order from the queue, and processes the
* next one (if any). Returns false and defaults to IDLE
* if there are no remaining orders.
*/
UnitAI.prototype.FinishOrder = function()
{
if (!this.orderQueue.length)
{
var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(this.entity);
error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
}
this.orderQueue.shift();
this.order = this.orderQueue[0];
if (this.orderQueue.length)
{
var ret = this.UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
{
return this.FinishOrder();
}
// Otherwise we've successfully processed a new order
return true;
}
else
{
this.SetNextState("IDLE");
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// Check if there are queued formation orders
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
// Inform the formation controller that we finished this task
this.finishedOrder = true;
// We don't want to carry out the default order
// if there are still queued formation orders left
if (cmpUnitAI.GetOrders().length > 1)
return true;
}
}
return false;
}
};
/**
* Add an order onto the back of the queue,
* and execute it if we didn't already have an order.
*/
UnitAI.prototype.PushOrder = function(type, data)
{
var order = { "type": type, "data": data };
this.orderQueue.push(order);
// If we didn't already have an order, then process this new one
if (this.orderQueue.length == 1)
{
this.order = order;
var ret = this.UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
this.FinishOrder();
}
};
/**
* Add an order onto the front of the queue,
* and execute it immediately.
*/
UnitAI.prototype.PushOrderFront = function(type, data)
{
var order = { "type": type, "data": data };
// If current order is cheering then add new order after it
// same thing if current order if packing/unpacking
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue.unshift(cheeringOrder, order);
}
else if (this.order && this.IsPacking())
{
var packingOrder = this.orderQueue.shift();
this.orderQueue.unshift(packingOrder, order);
}
else
{
this.orderQueue.unshift(order);
this.order = order;
var ret = this.UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off again;
// assume the previous active order is still valid (the short-lived
// new order hasn't changed state or anything) so we can carry on
// as if nothing had happened
if (ret && ret.discardOrder)
{
this.orderQueue.shift();
this.order = this.orderQueue[0];
}
}
};
/**
* Insert an order after the last forced order onto the queue
* and after the other orders of the same type
*/
UnitAI.prototype.PushOrderAfterForced = function(type, data)
{
if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type))
{
this.PushOrderFront(type, data);
}
else
{
for (var i = 1; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
continue;
if (this.orderQueue[i].type == type)
continue;
this.orderQueue.splice(i, 0, {"type": type, "data": data});
return;
}
this.PushOrder(type, data);
}
};
UnitAI.prototype.ReplaceOrder = function(type, data)
{
// Remember the previous work orders to be able to go back to them later if required
if (data && data.force)
{
if (this.IsFormationController())
this.CallMemberFunction("UpdateWorkOrders", [type]);
else
this.UpdateWorkOrders(type);
}
// Special cases of orders that shouldn't be replaced:
// 1. Cheering - we're invulnerable, add order after we finish
// 2. Packing/unpacking - we're immobile, add order after we finish (unless it's cancel)
// TODO: maybe a better way of doing this would be to use priority levels
if (this.order && this.order.type == "Cheering")
{
var order = { "type": type, "data": data };
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder, order];
}
else if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack")
{
var order = { "type": type, "data": data };
var packingOrder = this.orderQueue.shift();
this.orderQueue = [packingOrder, order];
}
else
{
this.orderQueue = [];
this.PushOrder(type, data);
}
};
UnitAI.prototype.GetOrders = function()
{
return this.orderQueue.slice();
};
UnitAI.prototype.AddOrders = function(orders)
{
for each (var order in orders)
{
this.PushOrder(order.type, order.data);
}
};
UnitAI.prototype.GetOrderData = function()
{
var orders = [];
for (var i in this.orderQueue)
{
if (this.orderQueue[i].data)
orders.push(deepcopy(this.orderQueue[i].data));
}
return orders;
};
UnitAI.prototype.UpdateWorkOrders = function(type)
{
// Under alert, remembered work orders won't be forgotten
if (this.IsUnderAlert())
return;
var isWorkType = function(type){
return (type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource");
};
// If we are being re-affected to a work order, forget the previous ones
if (isWorkType(type))
{
this.workOrders = [];
return;
}
// Then if we already have work orders, keep them
if (this.workOrders.length)
return;
// First if the unit is in a formation, get its workOrders from it
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i)
{
if (isWorkType(cmpUnitAI.orderQueue[i].type))
{
this.workOrders = cmpUnitAI.orderQueue.slice(i);
return;
}
}
}
}
// If nothing found, take the unit orders
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (isWorkType(this.orderQueue[i].type))
{
this.workOrders = this.orderQueue.slice(i);
return;
}
}
};
UnitAI.prototype.BackToWork = function()
{
if (this.workOrders.length == 0)
return false;
// Clear the order queue considering special orders not to avoid
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder];
}
else
this.orderQueue = [];
this.AddOrders(this.workOrders);
// And if the unit is in a formation, remove it from the formation
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers([this.entity]);
}
this.workOrders = [];
return true;
};
UnitAI.prototype.HasWorkOrders = function()
{
return this.workOrders.length > 0;
};
UnitAI.prototype.GetWorkOrders = function()
{
return this.workOrders;
};
UnitAI.prototype.SetWorkOrders = function(orders)
{
this.workOrders = orders;
};
UnitAI.prototype.TimerHandler = function(data, lateness)
{
// Reset the timer
if (data.timerRepeat === undefined)
{
this.timer = undefined;
}
this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
/**
* Set up the UnitAI timer to run after 'offset' msecs, and then
* every 'repeat' msecs until StopTimer is called. A "Timer" message
* will be sent each time the timer runs.
*/
UnitAI.prototype.StartTimer = function(offset, repeat)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var data = { "timerRepeat": repeat };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (repeat === undefined)
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
else
this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
};
/**
* Stop the current UnitAI timer.
*/
UnitAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
//// Message handlers /////
UnitAI.prototype.OnMotionChanged = function(msg)
{
if (msg.starting && !msg.error)
{
this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
}
else if (!msg.starting || msg.error)
{
this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
}
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
{
// TODO: This is a bit inefficient since every unit listens to every
// construction message - ideally we could scope it to only the one we're building
this.UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
};
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
for each (var order in this.orderQueue)
{
if (order.data && order.data.target && order.data.target == msg.entity)
order.data.target = msg.newentity;
if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
order.data.formationTarget = msg.newentity;
}
if (this.isGuardOf && this.isGuardOf == msg.entity)
this.isGuardOf = msg.newentity;
};
UnitAI.prototype.OnAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
};
UnitAI.prototype.OnGuardedAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
};
UnitAI.prototype.OnHealthChanged = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
};
UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
else if (msg.tag == this.losHealRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
};
UnitAI.prototype.OnPackFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
};
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.GetWalkSpeed();
};
UnitAI.prototype.GetRunSpeed = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
var runSpeed = cmpUnitMotion.GetRunSpeed();
var walkSpeed = cmpUnitMotion.GetWalkSpeed();
if (runSpeed <= walkSpeed)
return runSpeed;
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var health = cmpHealth.GetHitpoints()/cmpHealth.GetMaxHitpoints();
return (health*runSpeed + (1-health)*walkSpeed);
};
/**
* Returns true if the target exists and has non-zero hitpoints.
*/
UnitAI.prototype.TargetIsAlive = function(ent)
{
var cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
return true;
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (!cmpHealth)
return false;
return (cmpHealth.GetHitpoints() != 0);
};
/**
* Returns true if the target exists and needs to be killed before
* beginning to gather resources from it.
*/
UnitAI.prototype.MustKillGatherTarget = function(ent)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
if (!cmpResourceSupply.GetKillBeforeGather())
return false;
return this.TargetIsAlive(ent);
};
/**
* Returns the entity ID of the nearest resource supply where the given
* filter returns true, or undefined if none can be found.
* TODO: extend this to exclude resources that already have lots of
* gatherers.
*/
UnitAI.prototype.FindNearbyResource = function(filter)
{
var range = 64; // TODO: what's a sensible number?
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
// We accept resources owned by Gaia or any player
var players = [0];
for (var i = 1; i < playerMan.GetNumPlayers(); ++i)
players.push(i);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply);
for each (var ent in nearby)
{
if (!this.CanGather(ent))
continue;
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
var amount = cmpResourceSupply.GetCurrentAmount();
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
// Remove "resource|" prefix from template names, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
if (amount > 0 && cmpResourceSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity) && filter(ent, type, template))
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest resource dropsite that accepts
* the given type, or undefined if none can be found.
*/
UnitAI.prototype.FindNearestDropsite = function(genericType)
{
// Find dropsites owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players = [cmpOwnership.GetOwner()];
// Ships are unable to reach land dropsites and shouldn't attempt to do so.
var excludeLand = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
if (excludeLand)
{
nearby = nearby.filter( function(e) {
return Engine.QueryInterface(e, IID_Identity).HasClass("Naval");
});
}
for each (var ent in nearby)
{
var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (!cmpDropsite.AcceptsType(genericType))
continue;
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest building that needs to be constructed,
* or undefined if none can be found close enough.
*/
UnitAI.prototype.FindNearbyFoundation = function()
{
var range = 64; // TODO: what's a sensible number?
// Find buildings owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players = [cmpOwnership.GetOwner()];
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_Foundation);
for each (var ent in nearby)
{
// Skip foundations that are already complete. (This matters since
// we process the ConstructionFinished message before the foundation
// we're working on has been deleted.)
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation.IsFinished())
continue;
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest building in which the unit can garrison,
* or undefined if none can be found close enough.
*/
UnitAI.prototype.FindNearbyGarrisonHolder = function()
{
var range = 128; // TODO: what's a sensible number?
// Find buildings owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players = [cmpOwnership.GetOwner()];
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_GarrisonHolder);
for each (var ent in nearby)
{
var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
// We only want to garrison in buildings, not in moving units like ships,...
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI && cmpGarrisonHolder.AllowedToGarrison(this.entity) && !cmpGarrisonHolder.IsFull())
return ent;
}
return undefined;
};
/**
* Play a sound appropriate to the current entity.
*/
UnitAI.prototype.PlaySound = function(name)
{
// If we're a formation controller, use the sounds from our first member
if (this.IsFormationController())
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
var member = cmpFormation.GetPrimaryMember();
if (member)
PlaySound(name, member);
}
else
{
// Otherwise use our own sounds
PlaySound(name, this.entity);
}
};
UnitAI.prototype.SetGathererAnimationOverride = function(disable)
{
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return;
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
// Remove the animation override, so that weapons are shown again.
if (disable)
{
cmpVisual.ResetMoveAnimation("walk");
return;
}
// Work out what we're carrying, in order to select an appropriate animation
var type = cmpResourceGatherer.GetLastCarriedType();
if (type)
{
var typename = "carry_" + type.generic;
// Special case for meat
if (type.specific == "meat")
typename = "carry_" + type.specific;
cmpVisual.ReplaceMoveAnimation("walk", typename);
}
else
cmpVisual.ResetMoveAnimation("walk");
}
UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
// Special case: the "move" animation gets turned into a special
// movement mode that deals with speeds and walk/run automatically
if (name == "move")
{
// Speed to switch from walking to running animations
var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2;
cmpVisual.SelectMovementAnimation(runThreshold);
return;
}
var soundgroup;
if (sound)
{
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
soundgroup = cmpSound.GetSoundGroup(sound);
}
// Set default values if unspecified
if (once === undefined)
once = false;
if (speed === undefined)
speed = 1.0;
if (soundgroup === undefined)
soundgroup = "";
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
};
UnitAI.prototype.StopMoving = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.StopMoving();
};
UnitAI.prototype.MoveToPoint = function(x, z)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
};
UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
};
UnitAI.prototype.MoveToTarget = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
};
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Move unit so we hope the target is in the attack range
* for melee attacks, this goes straight to the default range checks
* for ranged attacks, the parabolic range is used
*/
UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
var cmpFormationAttack = Engine.QueryInterface(this.formationController, IID_Attack);
var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationAttack && cmpFormationAttack.CanAttackAsFormation() && cmpFormationUnitAI && cmpFormationUnitAI.GetCurrentState() == "FORMATIONCONTROLLER.ATTACKING")
return false;
}
var cmpFormation = Engine.QueryInterface(target, IID_Formation)
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if(type!= "Ranged")
return this.MoveToTargetRange(target, IID_Attack, type);
if (!this.CheckTargetVisible(target))
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(type);
var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
var s = thisCmpPosition.GetPosition();
var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if(!targetCmpPosition.IsInWorld())
return false;
var t = targetCmpPosition.GetPosition();
// h is positive when I'm higher than the target
var h = s.y-t.y+range.elevationBonus;
// No negative roots please
if(h>-range.max/2)
var parabolicMaxRange = Math.sqrt(range.max*range.max+2*range.max*h);
else
// return false? Or hope you come close enough?
var parabolicMaxRange = 0;
//return false;
// the parabole changes while walking, take something in the middle
var guessedMaxRange = (range.max + parabolicMaxRange)/2;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange))
return true;
// if that failed, try closer
return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange));
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
UnitAI.prototype.MoveToGarrisonRange = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInPointRange(x, z, min, max);
};
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};
/**
* Check if the target is inside the attack range
* For melee attacks, this goes straigt to the regular range calculation
* For ranged attacks, the parabolic formula is used to accout for bigger ranges
* when the target is lower, and smaller ranges when the target is higher
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
var cmpFormationAttack = Engine.QueryInterface(this.formationController, IID_Attack);
var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if
(
cmpFormationAttack &&
cmpFormationAttack.CanAttackAsFormation() &&
cmpFormationUnitAI &&
cmpFormationUnitAI.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING" &&
cmpFormationUnitAI.order.data.target == target
)
return true;
}
var cmpFormation = Engine.QueryInterface(target, IID_Formation)
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.CheckTargetRange(target, IID_Attack, type);
var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(type);
var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
var s = thisCmpPosition.GetPosition();
var t = targetCmpPosition.GetPosition();
var h = s.y-t.y+range.elevationBonus;
var maxRangeSq = 2*range.max*(h + range.max/2);
if (maxRangeSq < 0)
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq));
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, min, max);
};
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return false;
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden")
return false;
// Either visible directly, or visible in fog
return true;
};
UnitAI.prototype.FaceTowardsTarget = function(target)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
var targetpos = cmpTargetPosition.GetPosition();
var angle = Math.atan2(targetpos.x - pos.x, targetpos.z - pos.z);
var rot = cmpPosition.GetRotation();
var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI;
if (Math.abs(delta) > 0.2)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.z);
}
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type);
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return false;
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var halfvision = cmpVision.GetRange() / 2;
var pos = cmpPosition.GetPosition();
var heldPosition = this.heldPosition;
if (heldPosition === undefined)
heldPosition = {"x": pos.x, "z": pos.z};
var dx = heldPosition.x - pos.x;
var dz = heldPosition.z - pos.z;
var dist = Math.sqrt(dx*dx + dz*dz);
return dist < halfvision + range.max;
};
UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var range = cmpVision.GetRange();
var distance = DistanceBetweenEntities(this.entity,target);
return distance < range;
};
UnitAI.prototype.GetBestAttack = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttack();
};
UnitAI.prototype.GetBestAttackAgainst = function(target)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttackAgainst(target);
};
UnitAI.prototype.GetAttackBonus = function(type, target)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return 1;
return cmpAttack.GetAttackBonus(type, target);
};
/**
* Try to find one of the given entities which can be attacked,
* and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackVisibleEntity = function(ents, forceResponse)
{
for each (var target in ents)
{
if (this.CanAttack(target, forceResponse))
{
this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse });
return true;
}
}
return false;
};
/**
* Try to find one of the given entities which can be attacked
* and which is close to the hold position, and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackEntityInZone = function(ents, forceResponse)
{
for each (var target in ents)
{
var type = this.GetBestAttackAgainst(target);
if (this.CanAttack(target, forceResponse) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, type)
&& (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)))
{
this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse });
return true;
}
}
return false;
};
/**
* Try to respond appropriately given our current stance,
* given a list of entities that match our stance's target criteria.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
if (!ents.length)
return false;
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents, true);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents, true);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents, true);
if (this.GetStance().respondFlee)
{
this.PushOrderFront("Flee", { "target": ents[0], "force": false });
return true;
}
return false;
};
/**
* Try to respond to healable entities.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToHealableEntities = function(ents)
{
if (!ents.length)
return false;
for each (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
}
}
return false;
};
/**
* Returns true if we should stop following the target entity.
*/
UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type)
{
// Forced orders shouldn't be interrupted.
if (force)
return false;
// If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
var cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack)
{
for each (var targetType in cmpAttack.GetAttackTypes())
if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, targetType))
return false;
}
}
// Stop if we're in hold-ground mode and it's too far from the holding point
if (this.GetStance().respondHoldGround)
{
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type))
return true;
}
// Stop if it's left our vision range, unless we're especially persistent
if (!this.GetStance().respondChaseBeyondVision)
{
if (!this.CheckTargetIsInVisionRange(target))
return true;
}
// (Note that CCmpUnitMotion will detect if the target is lost in FoW,
// and will continue moving to its last seen position and then stop)
return false;
};
/*
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
if (this.IsTurret())
return false;
// TODO: use special stances instead?
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack)
return false;
if (this.GetStance().respondChase)
return true;
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
var cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack)
{
for each (var type in cmpAttack.GetAttackTypes())
if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))
return true;
}
}
if (force)
return true;
return false;
};
//// External interface functions ////
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.SetLastFormationTemplate = function(template)
{
this.lastFormationTemplate = template;
};
UnitAI.prototype.GetLastFormationTemplate = function()
{
return this.lastFormationTemplate;
};
UnitAI.prototype.MoveIntoFormation = function(cmd)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
// Add new order to move into formation at the current position
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
for (var i = 0; i < this.orderQueue.length; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "WalkAndFight":
case "WalkToPointRange":
case "MoveIntoFormation":
case "GatherNearPosition":
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
// Find the target unit's position
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return targetPositions;
targetPositions.push(cmpTargetPosition.GetPosition2D());
return targetPositions;
case "Stop":
return [];
default:
error("GetTargetPositions: Unrecognised order type '"+order.type+"'");
return [];
}
}
return targetPositions;
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
* Intended for Formation to switch to column layout on long walks.
*/
UnitAI.prototype.ComputeWalkingDistance = function()
{
var distance = 0;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return 0;
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition2D();
var targetPositions = this.GetTargetPositions();
for (var i = 0; i < targetPositions.length; i++)
{
distance += pos.distanceTo(targetPositions[i]);
// Remember this as the start position for the next order
pos = targetPositions[i];
}
// Return the total distance to the end of the order queue
return distance;
};
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (this.expectedRoute)
this.expectedRoute = undefined;
if (queued)
this.PushOrder(type, data);
else
this.ReplaceOrder(type, data);
};
/**
* Adds guard/escort order to the queue, forced by the player.
*/
UnitAI.prototype.Guard = function(target, queued)
{
if (!this.CanGuard())
{
this.WalkToTarget(target, queued);
return;
}
// if we already had an old guard order, do nothing if the target is the same
// and the order is running, otherwise remove the previous order
if (this.isGuardOf)
{
if (this.isGuardOf == target && this.order && this.order.type == "Guard")
return;
else
this.RemoveGuard();
}
this.AddOrder("Guard", { "target": target, "force": false }, queued);
};
UnitAI.prototype.AddGuard = function(target)
{
if (!this.CanGuard())
return false;
var cmpGuard = Engine.QueryInterface(target, IID_Guard);
if (!cmpGuard)
return false;
// Do not allow to guard a unit already guarding
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsGuardOf())
return false;
this.isGuardOf = target;
this.guardRange = cmpGuard.GetRange(this.entity);
cmpGuard.AddGuard(this.entity);
return true;
};
UnitAI.prototype.RemoveGuard = function()
{
if (this.isGuardOf)
{
var cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
if (cmpGuard)
cmpGuard.RemoveGuard(this.entity);
this.guardRange = undefined;
this.isGuardOf = undefined;
}
if (!this.order)
return;
if (this.order.type == "Guard")
this.UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"});
else
for (var i = 1; i < this.orderQueue.length; ++i)
if (this.orderQueue[i].type == "Guard")
this.orderQueue.splice(i, 1);
};
UnitAI.prototype.IsGuardOf = function()
{
return this.isGuardOf;
};
UnitAI.prototype.SetGuardOf = function(entity)
{
// entity may be undefined
this.isGuardOf = entity;
};
UnitAI.prototype.CanGuard = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Do not let a unit already guarded to guard. This would work in principle,
// but would clutter the gui with too much buttons to take all cases into account
var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
if (cmpGuard && cmpGuard.GetEntities().length)
return false;
return (this.template.CanGuard == "true");
};
/**
* Adds walk order to queue, forced by the player.
*/
UnitAI.prototype.Walk = function(x, z, queued)
{
if (this.expectedRoute && queued)
this.expectedRoute.push({ "x": x, "z": z });
else
this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds walk to point range order to queue, forced by the player.
*/
UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued)
{
this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued);
};
/**
* Adds stop order to queue, forced by the player.
*/
UnitAI.prototype.Stop = function(queued)
{
this.AddOrder("Stop", undefined, queued);
};
/**
* Adds walk-to-target order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkToTarget = function(target, queued)
{
this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued);
};
/**
* Adds walk-and-fight order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkAndFight = function(x, z, queued)
{
this.AddOrder("WalkAndFight", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds leave foundation order to queue, treated as forced.
*/
UnitAI.prototype.LeaveFoundation = function(target)
{
// If we're already being told to leave a foundation, then
// ignore this new request so we don't end up being too indecisive
// to ever actually move anywhere
// Ignore also the request if we are packing
if (this.order && (this.order.type == "LeaveFoundation" || this.IsPacking()))
return;
this.PushOrderFront("LeaveFoundation", { "target": target, "force": true });
};
/**
* Adds attack order to the queue, forced by the player.
*/
UnitAI.prototype.Attack = function(target, queued)
{
if (!this.CanAttack(target))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
this.MoveToTargetRange(target, IID_Heal);
else
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Attack", { "target": target, "force": true }, queued);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued)
{
if (target == this.entity)
return;
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true }, queued);
};
/**
* Adds ungarrison order to the queue.
*/
UnitAI.prototype.Ungarrison = function()
{
if (this.IsGarrisoned())
{
this.AddOrder("Ungarrison", null, false);
}
};
/**
* Adds autogarrison order to the queue (only used by ProductionQueue for auto-garrisoning
* and Promotion when promoting already garrisoned entities).
*/
UnitAI.prototype.Autogarrison = function(target)
{
this.AddOrder("Autogarrison", { "target": target }, false);
};
/**
* Adds gather order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Gather = function(target, queued)
{
this.PerformGather(target, queued, true);
};
/**
* Internal function to abstract the force parameter.
*/
UnitAI.prototype.PerformGather = function(target, queued, force)
{
if (!this.CanGather(target))
{
this.WalkToTarget(target, queued);
return;
}
// Save the resource type now, so if the resource gets destroyed
// before we process the order then we still know what resource
// type to look for more of
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
// Also save the target entity's template, so that if it's an animal,
// we won't go from hunting slow safe animals to dangerous fast ones
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(target);
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
// Remember the position of our target, if any, in case it disappears
// later and we want to head to its last known position
var lastPos = undefined;
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
lastPos = cmpPosition.GetPosition();
this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued);
};
/**
* Adds gather-near-position order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
{
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
};
/**
* Adds heal order to the queue, forced by the player.
*/
UnitAI.prototype.Heal = function(target, queued)
{
if (!this.CanHeal(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Heal", { "target": target, "force": true }, queued);
};
/**
* Adds return resource order to the queue, forced by the player.
*/
UnitAI.prototype.ReturnResource = function(target, queued)
{
if (!this.CanReturnResource(target, true))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
};
/**
* Adds trade order to the queue. Either walk to the first market, or
* start a new route. Not forced, so it can be interrupted by attacks.
* The possible route may be given directly as a SetupTradeRoute argument
* if coming from a RallyPoint, or through this.expectedRoute if a user command.
*/
UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued)
{
if (!this.CanTrade(target))
{
this.WalkToTarget(target, queued);
return;
}
var marketsChanged = this.SetTargetMarket(target, source);
if (marketsChanged)
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets())
{
var data = { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket(), "route": route, "force": false };
if (this.expectedRoute)
{
if (!route && this.expectedRoute.length)
data.route = this.expectedRoute.slice();
this.expectedRoute = undefined;
}
if (this.IsFormationController())
{
this.CallMemberFunction("AddOrder", ["Trade", data, queued]);
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.Disband();
}
else
this.AddOrder("Trade", data, queued);
}
else
{
if (this.IsFormationController())
this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]);
else
this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
this.expectedRoute = [];
}
}
};
UnitAI.prototype.SetTargetMarket = function(target, source)
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader)
return false;
var marketsChanged = cmpTrader.SetTargetMarket(target, source);
if (this.IsFormationController())
this.CallMemberFunction("SetTargetMarket", [target, source]);
return marketsChanged;
};
UnitAI.prototype.MoveToMarket = function(targetMarket)
{
if (this.waypoints && this.waypoints.length > 1)
{
var point = this.waypoints.pop();
var ok = this.MoveToPoint(point.x, point.z);
if (!ok)
ok = this.MoveToMarket(targetMarket);
}
else
{
this.waypoints = undefined;
var ok = this.MoveToTarget(targetMarket);
}
return ok;
};
UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket, nextMarket, nextFsmStateName)
{
if (!this.CanTrade(currentMarket))
{
this.StopTrading();
return;
}
if (this.CheckTargetRange(currentMarket, IID_Trader))
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.PerformTrade(currentMarket);
if (!cmpTrader.GetGain().traderGain)
{
this.StopTrading();
return;
}
if (this.order.data.route && this.order.data.route.length)
{
this.waypoints = this.order.data.route.slice();
if (nextFsmStateName == "APPROACHINGSECONDMARKET")
this.waypoints.reverse();
this.waypoints.unshift(null); // additionnal dummy point for the market
}
if (this.MoveToMarket(nextMarket)) // We've started walking to the next market
this.SetNextState(nextFsmStateName);
else
this.StopTrading();
}
else
{
if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again
this.StopTrading();
}
};
UnitAI.prototype.StopTrading = function()
{
this.StopMoving();
this.FinishOrder();
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.StopTrading();
};
/**
* Adds repair/build order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Repair = function(target, autocontinue, queued)
{
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued);
};
/**
* Adds flee order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.Flee = function(target, queued)
{
this.AddOrder("Flee", { "target": target, "force": false }, queued);
};
/**
* Adds cheer order to the queue. Forced so it won't be interrupted by attacks.
*/
UnitAI.prototype.Cheer = function()
{
this.AddOrder("Cheering", { "force": true }, false);
};
UnitAI.prototype.Pack = function(queued)
{
// Check that we can pack
if (this.CanPack())
this.AddOrder("Pack", { "force": true }, queued);
};
UnitAI.prototype.Unpack = function(queued)
{
// Check that we can unpack
if (this.CanUnpack())
this.AddOrder("Unpack", { "force": true }, queued);
};
UnitAI.prototype.CancelPack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
this.AddOrder("CancelPack", { "force": true }, queued);
};
UnitAI.prototype.CancelUnpack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
this.AddOrder("CancelUnpack", { "force": true }, queued);
};
UnitAI.prototype.SetStance = function(stance)
{
if (g_Stances[stance])
this.stance = stance;
else
error("UnitAI: Setting to invalid stance '"+stance+"'");
};
UnitAI.prototype.SwitchToStance = function(stance)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
this.SetStance(stance);
// Stop moving if switching to stand ground
// TODO: Also stop existing orders in a sensible way
if (stance == "standground")
this.StopMoving();
// Reset the range queries, since the range depends on stance.
this.SetupRangeQueries();
};
/**
* Resets losRangeQuery, and if there are some targets in range that we can
* attack then we start attacking and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewTargets = function()
{
if (!this.losRangeQuery)
return false;
if (!this.GetStance().targetVisibleEnemies)
return false;
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.AttackEntitiesByPreference( rangeMan.ResetActiveQuery(this.losRangeQuery) ))
return true;
return false;
};
UnitAI.prototype.FindWalkAndFightTargets = function()
{
if (this.IsFormationController())
{
var cmpUnitAI;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
for each (var ent in cmpFormation.members)
{
if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI)))
continue;
var targets = cmpUnitAI.GetTargetsFromUnit();
for each (var targ in targets)
{
if (cmpUnitAI.CanAttack(targ))
{
this.PushOrderFront("Attack", { "target": targ, "force": true });
return true;
}
}
}
return false;
}
var targets = this.GetTargetsFromUnit();
for each (var targ in targets)
{
if (this.CanAttack(targ))
{
this.PushOrderFront("Attack", { "target": targ, "force": true });
return true;
}
}
return false;
};
UnitAI.prototype.GetTargetsFromUnit = function()
{
if (!this.losRangeQuery)
return [];
if (!this.GetStance().targetVisibleEnemies)
return [];
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return [];
const attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var entities = rangeMan.ResetActiveQuery(this.losRangeQuery);
var targets = entities.filter(function (v) { return cmpAttack.CanAttack(v) && attackfilter(v); })
.sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); });
return targets;
};
/**
* Resets losHealRangeQuery, and if there are some targets in range that we can heal
* then we start healing and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewHealTargets = function()
{
if (!this.losHealRangeQuery)
return false;
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
for each (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
}
}
// We haven't found any target to heal
return false;
};
UnitAI.prototype.GetQueryRange = function(iid)
{
var ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
ret.min = range.min;
ret.max = range.max;
}
else if (this.GetStance().respondChase)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
else if (this.GetStance().respondHoldGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var halfvision = cmpVision.GetRange() / 2;
ret.max = range.max + halfvision;
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
return ret;
};
UnitAI.prototype.GetStance = function()
{
return g_Stances[this.stance];
};
+UnitAI.prototype.GetPossibleStances = function()
+{
+ return Object.keys(g_Stances);
+};
+
UnitAI.prototype.GetStanceName = function()
{
return this.stance;
};
UnitAI.prototype.SetMoveSpeed = function(speed)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.SetSpeed(speed);
};
UnitAI.prototype.SetHeldPosition = function(x, z)
{
this.heldPosition = {"x": x, "z": z};
};
UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
};
UnitAI.prototype.GetHeldPosition = function()
{
return this.heldPosition;
};
UnitAI.prototype.WalkToHeldPosition = function()
{
if (this.heldPosition)
{
this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false);
return true;
}
return false;
};
//// Helper functions ////
UnitAI.prototype.CanAttack = function(target, forceResponse)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Attack commands
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
if (!cmpAttack.CanAttack(target))
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
// Verify that the target is an attackable resource supply like a domestic animal
// or that it isn't owned by an ally of this entity's player or is responding to
// an attack.
var owner = cmpOwnership.GetOwner();
if (!this.MustKillGatherTarget(target)
&& !(IsOwnedByEnemyOfPlayer(owner, target)
|| IsOwnedByNeutralOfPlayer(owner, target)
|| (forceResponse && !IsOwnedByPlayer(owner, target))))
return false;
return true;
};
UnitAI.prototype.CanGarrison = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
// Verify that the target is owned by this entity's player or a mutual ally of this player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Don't let animals garrison for now
// (If we want to support that, we'll need to change Order.Garrison so it
// doesn't move the animal into an INVIDIDUAL.* state)
if (this.IsAnimal())
return false;
return true;
};
UnitAI.prototype.CanGather = function(target)
{
if (this.IsTurret())
return false;
// The target must be a valid resource supply.
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that we can gather from this target
if (!cmpResourceGatherer.GetTargetGatherRate(target))
return false;
// No need to verify ownership as we should be able to gather from
// a target regardless of ownership.
// No need to call "cmpResourceSupply.IsAvailable()" either because that
// would cause units to walk to full entities instead of choosing another one
// nearby to gather from, which is undesirable.
return true;
};
UnitAI.prototype.CanHeal = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Heal commands
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (!cmpHeal)
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
// Verify that the target is owned by the same player as the entity or of an ally
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Verify that the target is not unhealable (or at max health)
var cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.IsUnhealable())
return false;
// Verify that the target has no unhealable class
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetUnhealableClasses()))
return false;
// Verify that the target is a healable class
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetHealableClasses()))
return true;
return false;
};
UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to ReturnResource commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that the target is a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return false;
if (checkCarriedResource)
{
// Verify that we are carrying some resources,
// and can return our current resource to this target
var type = cmpResourceGatherer.GetMainCarryingType();
if (!type || !cmpResourceDropsite.AcceptsType(type))
return false;
}
// Verify that the dropsite is owned by this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return false;
return true;
};
UnitAI.prototype.CanTrade = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Trade commands
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || !cmpTrader.CanTrade(target))
return false;
return true;
};
UnitAI.prototype.CanRepair = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Repair (Builder) commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
return false;
// Verify that the target is owned by an ally of this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))
return false;
return true;
};
UnitAI.prototype.CanPack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return (cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked());
};
UnitAI.prototype.CanUnpack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return (cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked());
};
UnitAI.prototype.IsPacking = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return (cmpPack && cmpPack.IsPacking());
};
//// Animal specific functions ////
UnitAI.prototype.MoveRandomly = function(distance)
{
// We want to walk in a random direction, but avoid getting stuck
// in obstacles or narrow spaces.
// So pick a circular range from approximately our current position,
// and move outwards to the nearest point on that circle, which will
// lead to us avoiding obstacles and moving towards free space.
// TODO: we probably ought to have a 'home' point, and drift towards
// that, so we don't spread out all across the whole map
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return;
if (!cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
var jitter = 0.5;
// Randomly adjust the range's center a bit, so we tend to prefer
// moving in random directions (if there's nothing in the way)
var tx = pos.x + (2*Math.random()-1)*jitter;
var tz = pos.z + (2*Math.random()-1)*jitter;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.MoveToPointRange(tx, tz, distance, distance);
};
UnitAI.prototype.SetFacePointAfterMove = function(val)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion)
cmpMotion.SetFacePointAfterMove(val);
};
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
const attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
return this.RespondToTargetedEntities(
ents.filter(function (v) { return cmpAttack.CanAttack(v) && attackfilter(v); })
.sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); })
);
};
/**
* Call obj.funcname(args) on UnitAI components of all formation members.
*/
UnitAI.prototype.CallMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var members = cmpFormation.GetMembers();
for each (var ent in members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
}
};
/**
* Call obj.functname(args) on UnitAI components of all formation members,
* and return true if all calls return true.
*/
UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return false;
var members = cmpFormation.GetMembers();
for each (var ent in members)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI[funcname].apply(cmpUnitAI, args))
return false;
}
return true;
};
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);