Index: ps/trunk/binaries/data/mods/public/gui/session/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 19443)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 19444)
@@ -1,1843 +1,1828 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
const SDLK_LEFTBRACKET = 91;
const SDLK_RIGHTBRACKET = 93;
const SDLK_RSHIFT = 303;
const SDLK_LSHIFT = 304;
const SDLK_RCTRL = 305;
const SDLK_LCTRL = 306;
const SDLK_RALT = 307;
const SDLK_LALT = 308;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too
const ACTION_NONE = 0;
const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
const ACTION_GUARD = 3;
const ACTION_PATROL = 4;
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;
const INVALID_ENTITY = 0;
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
// 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;
+// Store the clicked entity on mousedown or mouseup
+var clickedEntity = 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 && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION))
{
let action = determineAction(mouseX, mouseY);
if (action)
{
if (action.cursor)
{
Engine.SetCursor(action.cursor);
cursorSet = true;
}
if (action.tooltip)
{
tooltipSet = true;
informationTooltip.caption = action.tooltip;
informationTooltip.hidden = false;
}
}
}
if (!cursorSet)
Engine.ResetCursor();
if (!tooltipSet)
informationTooltip.hidden = true;
var placementTooltip = Engine.GetGUIObjectByName("placementTooltip");
if (placementSupport.tooltipMessage)
placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip";
placementTooltip.caption = placementSupport.tooltipMessage || "";
placementTooltip.hidden = !placementSupport.tooltipMessage;
}
function updateBuildingPlacementPreview()
{
// The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or
// in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to.
// See onSimulationUpdate in session.js.
if (placementSupport.mode === "building")
{
if (placementSupport.template && placementSupport.position)
{
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)
if (result.pluralMessage)
message = translatePlural(result.message, result.pluralMessage, result.pluralCount);
else
message = translate(message);
var parameters = result.parameters;
if (result.translateParameters)
translateObjectKeys(parameters, result.translateParameters);
placementSupport.tooltipMessage = sprintf(message, parameters);
}
return false;
}
if (placementSupport.attack && placementSupport.attack.Ranged)
{
// 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.Ranged.maxRange,
"elevationBonus": placementSupport.attack.Ranged.elevationBonus,
};
var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range);
var range = Math.round(cmd.range);
placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" +
sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange });
}
return true;
}
}
else if (placementSupport.mode === "wall")
{
if (placementSupport.wallSet && placementSupport.position)
{
// Fetch an updated list of snapping candidate entities
placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities(
placementSupport.wallSet.templates.tower,
placementSupport.wallSnapEntitiesIncludeOffscreen,
true, // require exact template match
true // include foundations
);
return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
"wallSet": placementSupport.wallSet,
"start": placementSupport.position,
"end": placementSupport.wallEndPosition,
"snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment
});
}
}
return false;
}
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";
data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
cursor = "action-attack-move";
}
else if (Engine.HotkeyIsPressed("session.patrol"))
{
data.command = "patrol";
data.targetClasses = { "attack": g_PatrolTargets };
cursor = "action-patrol";
}
return { "possible": true, "data": data, "cursor": cursor };
}
return { "possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) > -1 };
}
// 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 (let entityID of selection)
{
var entState = GetExtendedEntityState(entityID);
if (!entState)
continue;
if (unitActions[action] && unitActions[action].getActionInfo)
{
var r = unitActions[action].getActionInfo(entState, targetState, simState);
if (r && r.possible) // 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 allOwnedByPlayer = selection.every(ent => {
var entState = GetEntityState(ent);
return entState && entState.player == g_ViewedPlayer;
});
if (!g_DevSettings.controlAll && !allOwnedByPlayer)
return undefined;
var target = undefined;
if (!fromMinimap)
{
var ent = Engine.PickEntityAtPoint(x, y);
if (ent != INVALID_ENTITY)
target = ent;
}
// decide between the following ordered actions
// if two actions are possible, the first one is taken
// so the most specific should appear first
var actions = Object.keys(unitActions).slice();
actions.sort((a, b) => 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("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'");
return false;
}
if (!updateBuildingPlacementPreview())
{
// invalid location - don't build it
// TODO: play a sound?
return false;
}
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "construct",
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed,
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued
});
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("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'");
return false;
}
var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
{
error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo));
return false;
}
if (!wallPlacementInfo)
return false;
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 (let piece of cmd.pieces)
{
if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
{
hasWallSegment = true;
break;
}
}
if (hasWallSegment)
{
Engine.PostNetworkCommand(cmd);
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
}
return true;
}
// Updates the bandbox object with new positions and visibility.
// The coordinates [x0, y0, x1, y1] are returned for further use.
function updateBandbox(bandbox, ev, hidden)
{
var x0 = dragStart[0];
var y0 = dragStart[1];
var x1 = ev.x;
var y1 = ev.y;
// normalize the orientation of the rectangle
if (x0 > x1) { let t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { let t = y0; y0 = y1; y1 = t; }
let scale = +Engine.ConfigDB_GetValue("user", "gui.scale");
bandbox.size = [x0 * scale, y0 * scale, x1 * scale, y1 * scale].join(" ");
bandbox.hidden = hidden;
return [x0, y0, x1, y1];
}
// Define some useful unit filters for getPreferredEntities
var unitFilters = {
"isUnit": entity => {
var entState = GetEntityState(entity);
return entState && hasClass(entState, "Unit");
},
"isDefensive": entity => {
var entState = GetEntityState(entity);
return entState && hasClass(entState, "Defensive");
},
"isMilitary": entity => {
var entState = GetEntityState(entity);
return entState &&
g_MilitaryTypes.some(c => hasClass(entState, c));
},
"isIdle": entity => {
var entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.unitAI &&
entState.unitAI.isIdle &&
!hasClass(entState, "Domestic");
},
"isAnything": entity => {
return true;
}
};
// Choose, inside a list of entities, which ones will be selected.
// We may use several entity filters, until one returns at least one element.
function getPreferredEntities(ents)
{
// Default filters
var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything];
// Handle hotkeys
if (Engine.HotkeyIsPressed("selection.milonly"))
filters = [unitFilters.isMilitary];
if (Engine.HotkeyIsPressed("selection.idleonly"))
filters = [unitFilters.isIdle];
var preferredEnts = [];
for (var i = 0; i < filters.length; ++i)
{
preferredEnts = ents.filter(filters[i]);
if (preferredEnts.length)
break;
}
return preferredEnts;
}
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:
var bandbox = Engine.GetGUIObjectByName("bandbox");
switch (ev.type)
{
case "mousemotion":
var rect = updateBandbox(bandbox, ev, false);
var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
var preferredEntities = getPreferredEntities(ents);
g_Selection.setHighlightList(preferredEntities);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var rect = updateBandbox(bandbox, ev, true);
// Get list of entities limited to preferred entities
var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
// 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
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)
{
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost });
placementSupport.tooltipMessage = [
getEntityCostTooltip(result),
getNeededResourcesTooltip(neededResources)
].filter(tip => tip).join("\n");
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
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")
{
g_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 == "session.timewarp.fastforward")
Engine.SetSimRate(20.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward")
Engine.SetSimRate(1.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind")
Engine.RewindTimeWarp();
}
if (ev.hotkey == "session.showstatusbars")
{
g_ShowAllStatusBars = (ev.type == "hotkeydown");
recalculateStatusBarDisplay();
}
else if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
else 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 ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
dragStart = [ ev.x, ev.y ];
inputState = INPUT_SELECTING;
+ // If a single click occured, reset the clickedEntity.
+ // Also set it if we're double/triple clicking and missed the unit earlier.
+ if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY)
+ clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
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 ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
if (!Engine.HotkeyIsPressed("session.queue"))
{
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 ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
- var ents = [];
- var selectedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
- if (selectedEntity == INVALID_ENTITY)
+ if (clickedEntity == INVALID_ENTITY)
+ clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
+ if (clickedEntity == INVALID_ENTITY)
{
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.reset();
resetIdleUnit();
}
inputState = INPUT_NORMAL;
return true;
}
- var now = new Date();
-
// If camera following and we select different unit, stop
- if (Engine.GetFollowedEntity() != selectedEntity)
- {
+ if (Engine.GetFollowedEntity() != clickedEntity)
Engine.CameraFollow(0);
- }
- if (now.getTime() - doubleClickTimer < doubleClickTime && selectedEntity == prevClickedEntity)
+ var ents = [];
+ if (ev.clicks == 1)
+ ents = [clickedEntity];
+ else
{
// 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 (ev.clicks == 2)
{
- // If double click hasn't already occurred, this is a double click.
// Select similar units regardless of rank
- templateToMatch = GetEntityState(selectedEntity).identity.selectionGroupName;
+ templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName;
if (templateToMatch)
matchRank = false;
else
// No selection group name defined, so fall back to exact match
- templateToMatch = GetEntityState(selectedEntity).template;
+ templateToMatch = GetEntityState(clickedEntity).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.
+ // Triple click
// Select units matching exact template name (same rank)
- templateToMatch = GetEntityState(selectedEntity).template;
+ templateToMatch = GetEntityState(clickedEntity).template;
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarPlayerEntities(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 = [selectedEntity];
- }
// 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
{
// cancel if not enough resources
if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost }))
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
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)
{
if (!controlsPlayer(g_ViewedPlayer))
return false;
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).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;
}
}
// 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 = [];
var jumpCameraLast;
var jumpCameraDistanceThreshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
function jumpCamera(index)
{
var position = jumpCameraPositions[index];
if (position)
{
if (jumpCameraLast &&
Math.abs(Engine.CameraGetX() - position.x) < jumpCameraDistanceThreshold &&
Math.abs(Engine.CameraGetZ() - position.z) < jumpCameraDistanceThreshold)
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 g_BatchTrainingEntities;
var g_BatchTrainingType;
var g_BatchTrainingCount;
var g_BatchTrainingEntityAllowedCount;
function flushTrainingBatch()
{
var appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType);
// If training limits don't allow us to train g_BatchTrainingCount in each appropriate building
if (g_BatchTrainingEntityAllowedCount !== undefined &&
g_BatchTrainingEntityAllowedCount < g_BatchTrainingCount * appropriateBuildings.length)
{
// Train as many full batches as we can
var buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / g_BatchTrainingCount);
var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch);
Engine.PostNetworkCommand({
"type": "train",
"entities": buildingsToTrainFullBatch,
"template": g_BatchTrainingType,
"count": g_BatchTrainingCount
});
// Train remainer in one more building
Engine.PostNetworkCommand({
"type": "train",
"entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ],
"template": g_BatchTrainingType,
"count": g_BatchTrainingEntityAllowedCount % g_BatchTrainingCount
});
}
else
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings,
"template": g_BatchTrainingType,
"count": g_BatchTrainingCount
});
}
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(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
};
if (!playerState.entityLimits)
return r;
var template = GetTemplateData(entType);
var entCategory = null;
if (template.trainingRestrictions)
entCategory = template.trainingRestrictions.category;
else if (template.buildRestrictions)
entCategory = template.buildRestrictions.category;
if (entCategory && playerState.entityLimits[entCategory] !== undefined)
{
r.entLimit = playerState.entityLimits[entCategory] || 0;
r.entCount = playerState.entityCounts[entCategory] || 0;
r.entLimitChangers = playerState.entityLimitChangers[entCategory];
r.canBeAddedCount = Math.max(r.entLimit - r.entCount, 0);
}
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 (!playerState || !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 limits = getEntityLimitAndCount(playerState, trainEntType);
// Batch training possible if we can train at least 2 units
var batchTrainingPossible = limits.canBeAddedCount == undefined || limits.canBeAddedCount > 1;
var decrement = Engine.HotkeyIsPressed("selection.remove");
if (!decrement)
var template = GetTemplateData(trainEntType);
let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
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 (g_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 (let i = 0; i < g_BatchTrainingEntities.length; ++i)
if (!(sameEnts = g_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 && g_BatchTrainingType == trainEntType)
{
if (decrement)
{
g_BatchTrainingCount -= batchIncrementSize;
if (g_BatchTrainingCount <= 0)
inputState = INPUT_NORMAL;
}
else if (limits.canBeAddedCount == undefined ||
limits.canBeAddedCount > g_BatchTrainingCount * appropriateBuildings.length)
{
if (Engine.GuiInterfaceCall("GetNeededResources", { "cost":
multiplyEntityCosts(template, g_BatchTrainingCount + batchIncrementSize) }))
return;
g_BatchTrainingCount += batchIncrementSize;
}
g_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", { "cost":
multiplyEntityCosts(template, batchIncrementSize) }))
return;
inputState = INPUT_BATCHTRAINING;
g_BatchTrainingEntities = selection;
g_BatchTrainingType = trainEntType;
g_BatchTrainingEntityAllowedCount = limits.canBeAddedCount;
g_BatchTrainingCount = batchIncrementSize;
}
else
{
// Non-batched - just create a single entity in each building
// (but no more than entity limit allows)
var buildingsForTraining = appropriateBuildings;
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 getTrainingStatus(playerState, trainEntType, selection)
{
let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
var appropriateBuildings = [];
if (selection)
appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
var nextBatchTrainingCount = 0;
var limits;
if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType)
{
nextBatchTrainingCount = g_BatchTrainingCount;
limits = {
"canBeAddedCount": g_BatchTrainingEntityAllowedCount
};
}
else
limits = getEntityLimitAndCount(playerState, trainEntType);
// We need to calculate count after the next increment if it's possible
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 (limits.canBeAddedCount !== undefined &&
limits.canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length)
{
buildingsCountToTrainFullBatch = Math.floor(limits.canBeAddedCount / nextBatchTrainingCount);
remainderToTrain = limits.canBeAddedCount % nextBatchTrainingCount;
}
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain];
}
// 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)
{
g_Selection.makePrimarySelection(templateName, Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup);
}
function performCommand(entState, commandName)
{
if (!entState)
return;
if (!controlsPlayer(entState.player) &&
!(g_IsObserver && commandName == "focus-rally"))
return;
if (g_EntityCommands[commandName])
g_EntityCommands[commandName].execute(entState);
}
function performAllyCommand(entity, commandName)
{
if (!entity)
return;
var entState = GetExtendedEntityState(entity);
var playerState = GetSimState().players[Engine.GetPlayerID()];
if (!playerState.isMutualAlly[entState.player] || g_IsObserver)
return;
if (g_AllyEntityCommands[commandName])
g_AllyEntityCommands[commandName].execute(entState);
}
function performFormation(entities, formationTemplate)
{
if (entities)
Engine.PostNetworkCommand({
"type": "formation",
"entities": entities,
"name": formationTemplate
});
}
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)
{
let entState = GetEntityState(toSelect[0]);
let position = entState.position;
if (position && entState.visibility != "hidden")
Engine.CameraMoveTo(position.x, position.z);
}
break;
case "save":
case "breakUp":
g_Groups.groups[groupId].reset();
if (action == "save")
g_Groups.addEntities(groupId, g_Selection.toList());
updateGroups();
break;
}
}
function performStance(entities, stanceName)
{
if (entities)
Engine.PostNetworkCommand({
"type": "stance",
"entities": entities,
"name": stanceName
});
}
function lockGate(lock)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "lock-gate",
"entities": selection,
"lock": lock,
});
}
function packUnit(pack)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "pack",
"entities": selection,
"pack": pack,
"queued": false
});
}
function cancelPackUnit(pack)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "cancel-pack",
"entities": selection,
"pack": pack,
"queued": false
});
}
function upgradeEntity(Template)
{
Engine.PostNetworkCommand({
"type": "upgrade",
"entities": g_Selection.toList(),
"template": Template,
"queued": false
});
}
function cancelUpgradeEntity()
{
Engine.PostNetworkCommand({
"type": "cancel-upgrade",
"entities": g_Selection.toList(),
"queued": false
});
}
// 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 currIdleClassIndex = 0;
var lastIdleClasses = [];
function resetIdleUnit()
{
lastIdleUnit = 0;
currIdleClassIndex = 0;
lastIdleClasses = [];
}
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.
if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i]))
resetIdleUnit();
lastIdleClasses = classes;
var data = {
"viewedPlayer": g_ViewedPlayer,
"excludeUnits": append ? g_Selection.toList() : [],
// If the current idle class index is not 0, put the class at that index first.
"idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex))
};
if (!selectall)
{
data.limit = 1;
data.prevUnit = lastIdleUnit;
}
var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
if (!idleUnits.length)
{
// TODO: display a message or play a sound to indicate no more idle units, or something
// Reset for next cycle
resetIdleUnit();
return;
}
if (!append)
g_Selection.reset();
g_Selection.addList(idleUnits);
if (selectall)
return;
lastIdleUnit = idleUnits[0];
var entityState = GetEntityState(lastIdleUnit);
var position = entityState.position;
if (position)
Engine.CameraMoveTo(position.x, position.z);
// Move the idle class index to the first class an idle unit was found for.
var indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem));
currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length;
}
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, owner)
{
// Filter out all entities that aren't garrisonable.
var garrisonHolders = g_Selection.toList().filter(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,
"owner": owner,
"garrisonHolders": garrisonHolders
});
}
function unloadSelection()
{
let parent = 0;
let ents = [];
for (let ent in g_Selection.selected)
{
let 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 unloadAllByOwner()
{
var garrisonHolders = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.garrisonHolder;
});
Engine.PostNetworkCommand({ "type": "unload-all-by-owner", "garrisonHolders": garrisonHolders });
}
function unloadAll()
{
// Filter out all entities that aren't garrisonable.
var garrisonHolders = g_Selection.toList().filter(e => {
var state = GetEntityState(e);
return state && state.garrisonHolder;
});
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(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(e => {
var state = GetEntityState(e);
return state && state.unitAI && state.unitAI.isGuarding;
});
Engine.PostNetworkCommand({ "type": "remove-guard", "entities": entities });
}
function raiseAlert()
{
let entities = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && state.alertRaiser && !state.alertRaiser.hasRaisedAlert;
});
Engine.PostNetworkCommand({ "type": "increase-alert-level", "entities": entities });
}
function increaseAlertLevel()
{
raiseAlert();
let entities = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && state.alertRaiser && state.alertRaiser.canIncreaseLevel;
});
Engine.PostNetworkCommand({ "type": "increase-alert-level", "entities": entities });
}
function endOfAlert()
{
let entities = g_Selection.toList().filter(e => {
let 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/source/gui/scripting/GuiScriptConversions.cpp
===================================================================
--- ps/trunk/source/gui/scripting/GuiScriptConversions.cpp (revision 19443)
+++ ps/trunk/source/gui/scripting/GuiScriptConversions.cpp (revision 19444)
@@ -1,144 +1,145 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "scriptinterface/ScriptConversions.h"
#include "gui/IGUIObject.h"
#include "lib/external_libraries/libsdl.h"
#include "ps/Hotkey.h"
#include "maths/Vector2D.h"
#define SET(obj, name, value) STMT(JS::RootedValue v_(cx); AssignOrToJSVal(cx, &v_, (value)); JS_SetProperty(cx, obj, (name), v_))
// ignore JS_SetProperty return value, because errors should be impossible
// and we can't do anything useful in the case of errors anyway
template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, SDL_Event_ const& val)
{
JSAutoRequest rq(cx);
const char* typeName;
switch (val.ev.type)
{
case SDL_WINDOWEVENT: typeName = "windowevent"; break;
case SDL_KEYDOWN: typeName = "keydown"; break;
case SDL_KEYUP: typeName = "keyup"; break;
case SDL_MOUSEMOTION: typeName = "mousemotion"; break;
case SDL_MOUSEBUTTONDOWN: typeName = "mousebuttondown"; break;
case SDL_MOUSEBUTTONUP: typeName = "mousebuttonup"; break;
case SDL_QUIT: typeName = "quit"; break;
case SDL_HOTKEYDOWN: typeName = "hotkeydown"; break;
case SDL_HOTKEYUP: typeName = "hotkeyup"; break;
default: typeName = "(unknown)"; break;
}
JS::RootedObject obj(cx, JS_NewPlainObject(cx));
if (!obj)
{
ret.setUndefined();
return;
}
SET(obj, "type", typeName);
switch (val.ev.type)
{
case SDL_KEYDOWN:
case SDL_KEYUP:
{
// SET(obj, "which", (int)val.ev.key.which); // (not in wsdl.h)
// SET(obj, "state", (int)val.ev.key.state); // (not in wsdl.h)
JS::RootedObject keysym(cx, JS_NewPlainObject(cx));
if (!keysym)
{
ret.setUndefined();
return;
}
JS::RootedValue keysymVal(cx, JS::ObjectValue(*keysym));
JS_SetProperty(cx, obj, "keysym", keysymVal);
// SET(keysym, "scancode", (int)val.ev.key.keysym.scancode); // (not in wsdl.h)
SET(keysym, "sym", (int)val.ev.key.keysym.sym);
// SET(keysym, "mod", (int)val.ev.key.keysym.mod); // (not in wsdl.h)
{
SET(keysym, "unicode", JS::UndefinedHandleValue);
}
// TODO: scripts have no idea what all the key/mod enum values are;
// we should probably expose them as constants if we expect scripts to use them
break;
}
case SDL_MOUSEMOTION:
{
// SET(obj, "which", (int)val.ev.motion.which); // (not in wsdl.h)
// SET(obj, "state", (int)val.ev.motion.state); // (not in wsdl.h)
SET(obj, "x", (int)val.ev.motion.x);
SET(obj, "y", (int)val.ev.motion.y);
// SET(obj, "xrel", (int)val.ev.motion.xrel); // (not in wsdl.h)
// SET(obj, "yrel", (int)val.ev.motion.yrel); // (not in wsdl.h)
break;
}
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
{
// SET(obj, "which", (int)val.ev.button.which); // (not in wsdl.h)
SET(obj, "button", (int)val.ev.button.button);
SET(obj, "state", (int)val.ev.button.state);
SET(obj, "x", (int)val.ev.button.x);
SET(obj, "y", (int)val.ev.button.y);
+ SET(obj, "clicks", (int)val.ev.button.clicks);
break;
}
case SDL_HOTKEYDOWN:
case SDL_HOTKEYUP:
{
SET(obj, "hotkey", static_cast(val.ev.user.data1));
break;
}
}
ret.setObject(*obj);
}
template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, IGUIObject* const& val)
{
if (val == NULL)
ret.setNull();
else
ret.setObject(*val->GetJSObject());
}
template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CGUIString& val)
{
ScriptInterface::ToJSVal(cx, ret, val.GetOriginalString());
}
template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CGUIString& out)
{
std::wstring val;
if (!ScriptInterface::FromJSVal(cx, v, val))
return false;
out.SetValue(val);
return true;
}
// define some vectors
JSVAL_VECTOR(CVector2D)
JSVAL_VECTOR(std::vector)
JSVAL_VECTOR(CGUIString)