1 const SDL_BUTTON_LEFT = 1;
2 const SDL_BUTTON_MIDDLE = 2;
3 const SDL_BUTTON_RIGHT = 3;
4 const SDLK_LEFTBRACKET = 91;
5 const SDLK_RIGHTBRACKET = 93;
6 const SDLK_RSHIFT = 303;
7 const SDLK_LSHIFT = 304;
8 const SDLK_RCTRL = 305;
9 const SDLK_LCTRL = 306;
10 const SDLK_RALT = 307;
11 const SDLK_LALT = 308;
12 // TODO: these constants should be defined somewhere else instead, in
13 // case any other code wants to use them too
15 const ACTION_NONE = 0;
16 const ACTION_GARRISON = 1;
17 const ACTION_REPAIR = 2;
18 const ACTION_GUARD = 3;
19 const ACTION_PATROL = 4;
20 var preSelectedAction = ACTION_NONE;
22 const INPUT_NORMAL = 0;
23 const INPUT_SELECTING = 1;
24 const INPUT_BANDBOXING = 2;
25 const INPUT_BUILDING_PLACEMENT = 3;
26 const INPUT_BUILDING_CLICK = 4;
27 const INPUT_BUILDING_DRAG = 5;
28 const INPUT_BATCHTRAINING = 6;
29 const INPUT_PRESELECTEDACTION = 7;
30 const INPUT_BUILDING_WALL_CLICK = 8;
31 const INPUT_BUILDING_WALL_PATHING = 9;
32 const INPUT_MASSTRIBUTING = 10;
34 var inputState = INPUT_NORMAL;
36 const INVALID_ENTITY = 0;
40 var mouseIsOverObject = false;
42 // Number of pixels the mouse can move before the action is considered a drag
45 // Time in milliseconds in which a double click is recognized
46 const doubleClickTime = 500;
47 var doubleClickTimer = 0;
48 var doubleClicked = false;
49 // Store the previously clicked entity - ensure a double/triple click happens on the same entity
50 var prevClickedEntity = 0;
52 // Same double-click behaviour for hotkey presses
53 const doublePressTime = 500;
54 var doublePressTimer = 0;
57 function updateCursorAndTooltip()
59 var cursorSet = false;
60 var tooltipSet = false;
61 var informationTooltip = Engine.GetGUIObjectByName("informationTooltip");
62 if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION))
64 let action = determineAction(mouseX, mouseY);
69 Engine.SetCursor(action.cursor);
75 informationTooltip.caption = action.tooltip;
76 informationTooltip.hidden = false;
85 informationTooltip.hidden = true;
87 var placementTooltip = Engine.GetGUIObjectByName("placementTooltip");
88 if (placementSupport.tooltipMessage)
89 placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip";
91 placementTooltip.caption = placementSupport.tooltipMessage || "";
92 placementTooltip.hidden = !placementSupport.tooltipMessage;
95 function updateBuildingPlacementPreview()
97 // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or
98 // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to.
99 // See onSimulationUpdate in session.js.
101 if (placementSupport.mode === "building")
103 if (placementSupport.template && placementSupport.position)
105 var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
106 "template": placementSupport.template,
107 "x": placementSupport.position.x,
108 "z": placementSupport.position.z,
109 "angle": placementSupport.angle,
110 "actorSeed": placementSupport.actorSeed
113 // Show placement info tooltip if invalid position
114 placementSupport.tooltipError = !result.success;
115 placementSupport.tooltipMessage = "";
119 if (result.message && result.parameters)
121 var message = result.message;
122 if (result.translateMessage)
123 if (result.pluralMessage)
124 message = translatePlural(result.message, result.pluralMessage, result.pluralCount);
126 message = translate(message);
127 var parameters = result.parameters;
128 if (result.translateParameters)
129 translateObjectKeys(parameters, result.translateParameters);
130 placementSupport.tooltipMessage = sprintf(message, parameters);
135 if (placementSupport.attack && placementSupport.attack.Ranged)
137 // building can be placed here, and has an attack
138 // show the range advantage in the tooltip
140 "x": placementSupport.position.x,
141 "z": placementSupport.position.z,
142 "range": placementSupport.attack.Ranged.maxRange,
143 "elevationBonus": placementSupport.attack.Ranged.elevationBonus,
145 var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range);
146 var range = Math.round(cmd.range);
147 placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" +
148 sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange });
153 else if (placementSupport.mode === "wall")
155 if (placementSupport.wallSet && placementSupport.position)
157 // Fetch an updated list of snapping candidate entities
158 placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities(
159 placementSupport.wallSet.templates.tower,
160 placementSupport.wallSnapEntitiesIncludeOffscreen,
161 true, // require exact template match
162 true // include foundations
165 return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
166 "wallSet": placementSupport.wallSet,
167 "start": placementSupport.position,
168 "end": placementSupport.wallEndPosition,
169 "snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment
177 function findGatherType(gatherer, supply)
179 if (!("resourceGatherRates" in gatherer) || !gatherer.resourceGatherRates || !supply)
181 if (gatherer.resourceGatherRates[supply.type.generic+"."+supply.type.specific])
182 return supply.type.specific;
183 if (gatherer.resourceGatherRates[supply.type.generic])
184 return supply.type.generic;
188 function getActionInfo(action, target)
190 var simState = GetSimState();
191 var selection = g_Selection.toList();
193 // If the selection doesn't exist, no action
194 var entState = GetEntityState(selection[0]);
196 return { "possible": false };
198 if (!target) // TODO move these non-target actions to an object like unit_actions.js
200 if (action == "set-rallypoint")
203 var data = { "command": "walk" };
204 if (Engine.HotkeyIsPressed("session.attackmove"))
206 data.command = "attack-walk";
207 data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
208 cursor = "action-attack-move";
210 else if (Engine.HotkeyIsPressed("session.patrol"))
212 data.command = "patrol";
213 data.targetClasses = { "attack": ["Unit"] };
214 cursor = "action-patrol";
216 return { "possible": true, "data": data, "cursor": cursor };
219 return { "possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) > -1 };
222 // Look at the first targeted entity
223 // (TODO: maybe we eventually want to look at more, and be more context-sensitive?
224 // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
225 var targetState = GetExtendedEntityState(target);
227 // Check if the target entity is a resource, dropsite, foundation, or enemy unit.
228 // Check if any entities in the selection can gather the requested resource,
229 // can return to the dropsite, can build the foundation, or can attack the enemy
230 for (let entityID of selection)
232 var entState = GetExtendedEntityState(entityID);
236 if (unitActions[action] && unitActions[action].getActionInfo)
238 var r = unitActions[action].getActionInfo(entState, targetState, simState);
239 if (r && r.possible) // return true if it's possible for one of the entities
243 return { "possible": false };
247 * Determine the context-sensitive action that should be performed when the mouse is at (x,y)
249 function determineAction(x, y, fromMinimap)
251 var selection = g_Selection.toList();
253 // No action if there's no selection
254 if (!selection.length)
256 preSelectedAction = ACTION_NONE;
260 // If the selection doesn't exist, no action
261 var entState = GetEntityState(selection[0]);
265 // If the selection isn't friendly units, no action
266 var allOwnedByPlayer = selection.every(ent => {
267 var entState = GetEntityState(ent);
268 return entState && entState.player == g_ViewedPlayer;
271 if (!g_DevSettings.controlAll && !allOwnedByPlayer)
274 var target = undefined;
277 var ent = Engine.PickEntityAtPoint(x, y);
278 if (ent != INVALID_ENTITY)
282 // decide between the following ordered actions
283 // if two actions are possible, the first one is taken
284 // so the most specific should appear first
285 var actions = Object.keys(unitActions).slice();
286 actions.sort((a, b) => unitActions[a].specificness - unitActions[b].specificness);
288 var actionInfo = undefined;
289 if (preSelectedAction != ACTION_NONE)
291 for (var action of actions)
292 if (unitActions[action].preSelectedActionCheck)
294 var r = unitActions[action].preSelectedActionCheck(target, selection);
299 return { "type": "none", "cursor": "", "target": target };
302 for (var action of actions)
303 if (unitActions[action].hotkeyActionCheck)
305 var r = unitActions[action].hotkeyActionCheck(target, selection);
310 for (var action of actions)
311 if (unitActions[action].actionCheck)
313 var r = unitActions[action].actionCheck(target, selection);
318 return { "type": "none", "cursor": "", "target": target };
322 var dragStart; // used for remembering mouse coordinates at start of drag operations
324 function tryPlaceBuilding(queued)
326 if (placementSupport.mode !== "building")
328 error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'");
332 if (!updateBuildingPlacementPreview())
334 // invalid location - don't build it
335 // TODO: play a sound?
339 var selection = g_Selection.toList();
341 Engine.PostNetworkCommand({
343 "template": placementSupport.template,
344 "x": placementSupport.position.x,
345 "z": placementSupport.position.z,
346 "angle": placementSupport.angle,
347 "actorSeed": placementSupport.actorSeed,
348 "entities": selection,
350 "autocontinue": true,
353 Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
356 placementSupport.Reset();
358 placementSupport.RandomizeActorSeed();
363 function tryPlaceWall(queued)
365 if (placementSupport.mode !== "wall")
367 error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'");
371 var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
372 if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
374 error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo));
378 if (!wallPlacementInfo)
381 var selection = g_Selection.toList();
383 "type": "construct-wall",
385 "autocontinue": true,
387 "entities": selection,
388 "wallSet": placementSupport.wallSet,
389 "pieces": wallPlacementInfo.pieces,
390 "startSnappedEntity": wallPlacementInfo.startSnappedEnt,
391 "endSnappedEntity": wallPlacementInfo.endSnappedEnt,
394 // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end
395 // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed
396 // (this is somewhat non-ideal and hardcode-ish)
397 var hasWallSegment = false;
398 for (let piece of cmd.pieces)
400 if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
402 hasWallSegment = true;
409 Engine.PostNetworkCommand(cmd);
410 Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
416 // Updates the bandbox object with new positions and visibility.
417 // The coordinates [x0, y0, x1, y1] are returned for further use.
418 function updateBandbox(bandbox, ev, hidden)
420 var x0 = dragStart[0];
421 var y0 = dragStart[1];
424 // normalize the orientation of the rectangle
425 if (x0 > x1) { let t = x0; x0 = x1; x1 = t; }
426 if (y0 > y1) { let t = y0; y0 = y1; y1 = t; }
428 bandbox.size = [x0, y0, x1, y1].join(" ");
429 bandbox.hidden = hidden;
431 return [x0, y0, x1, y1];
434 // Define some useful unit filters for getPreferredEntities
436 "isUnit": entity => {
437 var entState = GetEntityState(entity);
438 return entState && hasClass(entState, "Unit");
440 "isDefensive": entity => {
441 var entState = GetEntityState(entity);
442 return entState && hasClass(entState, "Defensive");
444 "isMilitary": entity => {
445 var entState = GetEntityState(entity);
447 g_MilitaryTypes.some(c => hasClass(entState, c));
449 "isIdle": entity => {
450 var entState = GetEntityState(entity);
453 hasClass(entState, "Unit") &&
455 entState.unitAI.isIdle &&
456 !hasClass(entState, "Domestic");
458 "isAnything": entity => {
463 // Choose, inside a list of entities, which ones will be selected.
464 // We may use several entity filters, until one returns at least one element.
465 function getPreferredEntities(ents)
468 var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything];
471 if (Engine.HotkeyIsPressed("selection.milonly"))
472 filters = [unitFilters.isMilitary];
473 if (Engine.HotkeyIsPressed("selection.idleonly"))
474 filters = [unitFilters.isIdle];
476 var preferredEnts = [];
477 for (var i = 0; i < filters.length; ++i)
479 preferredEnts = ents.filter(filters[i]);
480 if (preferredEnts.length)
483 return preferredEnts;
486 function handleInputBeforeGui(ev, hoveredObject)
488 // Capture mouse position so we can use it for displaying cursors,
492 case "mousebuttonup":
493 case "mousebuttondown":
500 // Remember whether the mouse is over a GUI object or not
501 mouseIsOverObject = (hoveredObject != null);
503 // Close the menu when interacting with the game world
504 if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown")
505 && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT))
508 // State-machine processing:
510 // (This is for states which should override the normal GUI processing - events will
511 // be processed here before being passed on, and propagation will stop if this function
514 // TODO: it'd probably be nice to have a better state-machine system, with guaranteed
515 // entry/exit functions, since this is a bit broken now
519 case INPUT_BANDBOXING:
520 var bandbox = Engine.GetGUIObjectByName("bandbox");
524 var rect = updateBandbox(bandbox, ev, false);
526 var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
527 var preferredEntities = getPreferredEntities(ents);
528 g_Selection.setHighlightList(preferredEntities);
532 case "mousebuttonup":
533 if (ev.button == SDL_BUTTON_LEFT)
535 var rect = updateBandbox(bandbox, ev, true);
537 // Get list of entities limited to preferred entities
538 var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
540 // Remove the bandbox hover highlighting
541 g_Selection.setHighlightList([]);
543 // Update the list of selected units
544 if (Engine.HotkeyIsPressed("selection.add"))
546 g_Selection.addList(ents);
548 else if (Engine.HotkeyIsPressed("selection.remove"))
550 g_Selection.removeList(ents);
555 g_Selection.addList(ents);
558 inputState = INPUT_NORMAL;
561 else if (ev.button == SDL_BUTTON_RIGHT)
564 bandbox.hidden = true;
566 g_Selection.setHighlightList([]);
568 inputState = INPUT_NORMAL;
575 case INPUT_BUILDING_CLICK:
579 // If the mouse moved far enough from the original click location,
580 // then switch to drag-orientation mode
581 var dragDeltaX = ev.x - dragStart[0];
582 var dragDeltaY = ev.y - dragStart[1];
583 var maxDragDelta = 16;
584 if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
586 inputState = INPUT_BUILDING_DRAG;
591 case "mousebuttonup":
592 if (ev.button == SDL_BUTTON_LEFT)
594 // If shift is down, let the player continue placing another of the same building
595 var queued = Engine.HotkeyIsPressed("session.queue");
596 if (tryPlaceBuilding(queued))
599 inputState = INPUT_BUILDING_PLACEMENT;
601 inputState = INPUT_NORMAL;
605 inputState = INPUT_BUILDING_PLACEMENT;
611 case "mousebuttondown":
612 if (ev.button == SDL_BUTTON_RIGHT)
615 placementSupport.Reset();
616 inputState = INPUT_NORMAL;
623 case INPUT_BUILDING_WALL_CLICK:
624 // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point
625 // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode.
628 case "mousebuttonup":
629 if (ev.button === SDL_BUTTON_LEFT)
631 inputState = INPUT_BUILDING_WALL_PATHING;
636 case "mousebuttondown":
637 if (ev.button == SDL_BUTTON_RIGHT)
640 placementSupport.Reset();
641 updateBuildingPlacementPreview();
643 inputState = INPUT_NORMAL;
650 case INPUT_BUILDING_WALL_PATHING:
651 // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
652 // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
653 // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
654 // user to continue building walls.
658 placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
660 // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!)
661 // the ending point and the starting point to snap to.
663 // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
664 // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
665 // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
666 // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
667 // in them. Might be useful to query only for entities within a certain range around the starting point and ending
670 placementSupport.wallSnapEntitiesIncludeOffscreen = true;
671 var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
673 if (result && result.cost)
675 var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost });
676 placementSupport.tooltipMessage = [
677 getEntityCostTooltip(result),
678 getNeededResourcesTooltip(neededResources)
679 ].filter(tip => tip).join("\n");
684 case "mousebuttondown":
685 if (ev.button == SDL_BUTTON_LEFT)
687 var queued = Engine.HotkeyIsPressed("session.queue");
688 if (tryPlaceWall(queued))
692 // continue building, just set a new starting position where we left off
693 placementSupport.position = placementSupport.wallEndPosition;
694 placementSupport.wallEndPosition = undefined;
696 inputState = INPUT_BUILDING_WALL_CLICK;
700 placementSupport.Reset();
701 inputState = INPUT_NORMAL;
705 placementSupport.tooltipMessage = translate("Cannot build wall here!");
707 updateBuildingPlacementPreview();
710 else if (ev.button == SDL_BUTTON_RIGHT)
712 // reset to normal input mode
713 placementSupport.Reset();
714 updateBuildingPlacementPreview();
716 inputState = INPUT_NORMAL;
723 case INPUT_BUILDING_DRAG:
727 var dragDeltaX = ev.x - dragStart[0];
728 var dragDeltaY = ev.y - dragStart[1];
729 var maxDragDelta = 16;
730 if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
732 // Rotate in the direction of the mouse
733 var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
734 placementSupport.angle = Math.atan2(target.x - placementSupport.position.x, target.z - placementSupport.position.z);
738 // If the mouse is near the center, snap back to the default orientation
739 placementSupport.SetDefaultAngle();
742 var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
743 "template": placementSupport.template,
744 "x": placementSupport.position.x,
745 "z": placementSupport.position.z
749 placementSupport.angle = snapData.angle;
750 placementSupport.position.x = snapData.x;
751 placementSupport.position.z = snapData.z;
754 updateBuildingPlacementPreview();
757 case "mousebuttonup":
758 if (ev.button == SDL_BUTTON_LEFT)
760 // If shift is down, let the player continue placing another of the same building
761 var queued = Engine.HotkeyIsPressed("session.queue");
762 if (tryPlaceBuilding(queued))
765 inputState = INPUT_BUILDING_PLACEMENT;
767 inputState = INPUT_NORMAL;
771 inputState = INPUT_BUILDING_PLACEMENT;
777 case "mousebuttondown":
778 if (ev.button == SDL_BUTTON_RIGHT)
781 placementSupport.Reset();
782 inputState = INPUT_NORMAL;
789 case INPUT_MASSTRIBUTING:
790 if (ev.type == "hotkeyup" && ev.hotkey == "session.masstribute")
793 inputState = INPUT_NORMAL;
797 case INPUT_BATCHTRAINING:
798 if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain")
800 flushTrainingBatch();
801 inputState = INPUT_NORMAL;
809 function handleInputAfterGui(ev)
811 if (ev.hotkey === undefined)
814 // Handle the time-warp testing features, restricted to single-player
815 if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked)
817 if (ev.type == "hotkeydown" && ev.hotkey == "session.timewarp.fastforward")
818 Engine.SetSimRate(20.0);
819 else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward")
820 Engine.SetSimRate(1.0);
821 else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind")
822 Engine.RewindTimeWarp();
825 if (ev.hotkey == "session.showstatusbars")
827 g_ShowAllStatusBars = (ev.type == "hotkeydown");
828 recalculateStatusBarDisplay();
830 else if (ev.hotkey == "session.highlightguarding")
832 g_ShowGuarding = (ev.type == "hotkeydown");
833 updateAdditionalHighlight();
835 else if (ev.hotkey == "session.highlightguarded")
837 g_ShowGuarded = (ev.type == "hotkeydown");
838 updateAdditionalHighlight();
841 // State-machine processing:
849 // Highlight the first hovered entity (if any)
850 var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
851 if (ent != INVALID_ENTITY)
852 g_Selection.setHighlightList([ent]);
854 g_Selection.setHighlightList([]);
858 case "mousebuttondown":
859 if (ev.button == SDL_BUTTON_LEFT)
861 dragStart = [ ev.x, ev.y ];
862 inputState = INPUT_SELECTING;
865 else if (ev.button == SDL_BUTTON_RIGHT)
867 var action = determineAction(ev.x, ev.y);
870 return doAction(action, ev);
875 if (ev.hotkey.indexOf("selection.group.") == 0)
877 var now = new Date();
878 if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey))
880 if (ev.hotkey.indexOf("selection.group.select.") == 0)
882 var sptr = ev.hotkey.split(".");
883 performGroup("snap", sptr[3]);
888 var sptr = ev.hotkey.split(".");
889 performGroup(sptr[2], sptr[3]);
891 doublePressTimer = now.getTime();
892 prevHotkey = ev.hotkey;
899 case INPUT_PRESELECTEDACTION:
903 // Highlight the first hovered entity (if any)
904 var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
905 if (ent != INVALID_ENTITY)
906 g_Selection.setHighlightList([ent]);
908 g_Selection.setHighlightList([]);
912 case "mousebuttondown":
913 if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
915 var action = determineAction(ev.x, ev.y);
918 if (!Engine.HotkeyIsPressed("session.queue"))
920 preSelectedAction = ACTION_NONE;
921 inputState = INPUT_NORMAL;
923 return doAction(action, ev);
925 else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
927 preSelectedAction = ACTION_NONE;
928 inputState = INPUT_NORMAL;
933 // Slight hack: If selection is empty, reset the input state
934 if (g_Selection.toList().length == 0)
936 preSelectedAction = ACTION_NONE;
937 inputState = INPUT_NORMAL;
943 case INPUT_SELECTING:
947 // If the mouse moved further than a limit, switch to bandbox mode
948 var dragDeltaX = ev.x - dragStart[0];
949 var dragDeltaY = ev.y - dragStart[1];
951 if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
953 inputState = INPUT_BANDBOXING;
957 var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
958 if (ent != INVALID_ENTITY)
959 g_Selection.setHighlightList([ent]);
961 g_Selection.setHighlightList([]);
964 case "mousebuttonup":
965 if (ev.button == SDL_BUTTON_LEFT)
968 var selectedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
969 if (selectedEntity == INVALID_ENTITY)
971 if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
976 inputState = INPUT_NORMAL;
980 var now = new Date();
982 // If camera following and we select different unit, stop
983 if (Engine.GetFollowedEntity() != selectedEntity)
985 Engine.CameraFollow(0);
988 if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity))
990 // Double click or triple click has occurred
991 var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
992 var matchRank = true;
995 // Check for double click or triple click
998 // If double click hasn't already occurred, this is a double click.
999 // Select similar units regardless of rank
1000 templateToMatch = GetEntityState(selectedEntity).identity.selectionGroupName;
1001 if (templateToMatch)
1006 { // No selection group name defined, so fall back to exact match
1007 templateToMatch = GetEntityState(selectedEntity).template;
1010 doubleClicked = true;
1011 // Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click
1012 doubleClickTimer = now.getTime();
1016 // Double click has already occurred, so this is a triple click.
1017 // Select units matching exact template name (same rank)
1018 templateToMatch = GetEntityState(selectedEntity).template;
1021 // TODO: Should we handle "control all units" here as well?
1022 ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
1026 // It's single click right now but it may become double or triple click
1027 doubleClicked = false;
1028 doubleClickTimer = now.getTime();
1029 prevClickedEntity = selectedEntity;
1031 // We only want to include the first picked unit in the selection
1032 ents = [selectedEntity];
1035 // Update the list of selected units
1036 if (Engine.HotkeyIsPressed("selection.add"))
1038 g_Selection.addList(ents);
1040 else if (Engine.HotkeyIsPressed("selection.remove"))
1042 g_Selection.removeList(ents);
1046 g_Selection.reset();
1047 g_Selection.addList(ents);
1050 inputState = INPUT_NORMAL;
1057 case INPUT_BUILDING_PLACEMENT:
1062 placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
1064 if (placementSupport.mode === "wall")
1066 // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is
1067 // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities
1068 // itself happens in the call to updateBuildingPlacementPreview below).
1069 placementSupport.wallSnapEntitiesIncludeOffscreen = false;
1073 // cancel if not enough resources
1074 if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost }))
1076 placementSupport.Reset();
1077 inputState = INPUT_NORMAL;
1081 var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
1082 "template": placementSupport.template,
1083 "x": placementSupport.position.x,
1084 "z": placementSupport.position.z,
1088 placementSupport.angle = snapData.angle;
1089 placementSupport.position.x = snapData.x;
1090 placementSupport.position.z = snapData.z;
1094 updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
1095 return false; // continue processing mouse motion
1097 case "mousebuttondown":
1098 if (ev.button == SDL_BUTTON_LEFT)
1100 if (placementSupport.mode === "wall")
1102 var validPlacement = updateBuildingPlacementPreview();
1103 if (validPlacement !== false)
1104 inputState = INPUT_BUILDING_WALL_CLICK;
1108 placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
1109 dragStart = [ ev.x, ev.y ];
1110 inputState = INPUT_BUILDING_CLICK;
1114 else if (ev.button == SDL_BUTTON_RIGHT)
1117 placementSupport.Reset();
1118 inputState = INPUT_NORMAL;
1125 var rotation_step = Math.PI / 12; // 24 clicks make a full rotation
1129 case "session.rotate.cw":
1130 placementSupport.angle += rotation_step;
1131 updateBuildingPlacementPreview();
1133 case "session.rotate.ccw":
1134 placementSupport.angle -= rotation_step;
1135 updateBuildingPlacementPreview();
1146 function doAction(action, ev)
1148 if (!controlsPlayer(g_ViewedPlayer))
1151 var selection = g_Selection.toList();
1153 // If shift is down, add the order to the unit's order queue instead
1154 // of running it immediately
1155 var queued = Engine.HotkeyIsPressed("session.queue");
1156 var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
1158 if (unitActions[action.type] && unitActions[action.type].execute)
1159 return unitActions[action.type].execute(target, action, selection, queued);
1161 error("Invalid action.type " + action.type);
1165 function handleMinimapEvent(target)
1167 // Partly duplicated from handleInputAfterGui(), but with the input being
1168 // world coordinates instead of screen coordinates.
1170 if (inputState != INPUT_NORMAL)
1173 var fromMinimap = true;
1174 var action = determineAction(undefined, undefined, fromMinimap);
1178 var selection = g_Selection.toList();
1180 var queued = Engine.HotkeyIsPressed("session.queue");
1181 if (unitActions[action.type] && unitActions[action.type].execute)
1182 return unitActions[action.type].execute(target, action, selection, queued);
1183 error("Invalid action.type " + action.type);
1187 // Called by GUI when user clicks construction button
1188 // @param buildTemplate Template name of the entity the user wants to build
1189 function startBuildingPlacement(buildTemplate, playerState)
1191 if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0)
1194 // TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI
1195 // to start building a structure, then the highlight selection rings are kept during the construction of the building.
1196 // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing.
1198 placementSupport.Reset();
1200 // find out if we're building a wall, and change the entity appropriately if so
1201 var templateData = GetTemplateData(buildTemplate);
1202 if (templateData.wallSet)
1204 placementSupport.mode = "wall";
1205 placementSupport.wallSet = templateData.wallSet;
1206 inputState = INPUT_BUILDING_PLACEMENT;
1210 placementSupport.mode = "building";
1211 placementSupport.template = buildTemplate;
1212 inputState = INPUT_BUILDING_PLACEMENT;
1215 if (templateData.attack &&
1216 templateData.attack.Ranged &&
1217 templateData.attack.Ranged.maxRange)
1219 // add attack information to display a good tooltip
1220 placementSupport.attack = templateData.attack;
1224 // Camera jumping: when the user presses a hotkey the current camera location is marked.
1225 // When they press another hotkey the camera jumps back to that position. If the camera is already roughly at that location,
1226 // jump back to where it was previously.
1227 var jumpCameraPositions = [];
1229 var jumpCameraDistanceThreshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
1231 function jumpCamera(index)
1233 var position = jumpCameraPositions[index];
1236 if (jumpCameraLast &&
1237 Math.abs(Engine.CameraGetX() - position.x) < jumpCameraDistanceThreshold &&
1238 Math.abs(Engine.CameraGetZ() - position.z) < jumpCameraDistanceThreshold)
1239 Engine.CameraMoveTo(jumpCameraLast.x, jumpCameraLast.z);
1242 jumpCameraLast = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
1243 Engine.CameraMoveTo(position.x, position.z);
1248 function setJumpCamera(index)
1250 jumpCameraPositions[index] = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
1254 // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
1255 // When the user releases shift, or clicks on a different training button, we create the batched units
1256 var batchTrainingEntities;
1257 var batchTrainingType;
1258 var batchTrainingCount;
1259 var batchTrainingEntityAllowedCount;
1261 function flushTrainingBatch()
1263 var appropriateBuildings = getBuildingsWhichCanTrainEntity(batchTrainingEntities, batchTrainingType);
1264 // If training limits don't allow us to train batchTrainingCount in each appropriate building
1265 if (batchTrainingEntityAllowedCount !== undefined &&
1266 batchTrainingEntityAllowedCount < batchTrainingCount * appropriateBuildings.length)
1268 // Train as many full batches as we can
1269 var buildingsCountToTrainFullBatch = Math.floor(batchTrainingEntityAllowedCount / batchTrainingCount);
1270 var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch);
1271 Engine.PostNetworkCommand({
1273 "entities": buildingsToTrainFullBatch,
1274 "template": batchTrainingType,
1275 "count": batchTrainingCount
1278 // Train remainer in one more building
1279 Engine.PostNetworkCommand({
1281 "entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ],
1282 "template": batchTrainingType,
1283 "count": batchTrainingEntityAllowedCount % batchTrainingCount
1287 Engine.PostNetworkCommand({
1289 "entities": appropriateBuildings,
1290 "template": batchTrainingType,
1291 "count": batchTrainingCount
1295 function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
1297 return entitiesToCheck.filter(entity => {
1298 var state = GetEntityState(entity);
1299 var canTrain = state && state.production && state.production.entities.length &&
1300 state.production.entities.indexOf(trainEntType) != -1;
1305 function getEntityLimitAndCount(playerState, entType)
1308 "entLimit": undefined,
1309 "entCount": undefined,
1310 "entLimitChangers": undefined,
1311 "canBeAddedCount": undefined
1313 if (!playerState.entityLimits)
1315 var template = GetTemplateData(entType);
1316 var entCategory = null;
1317 if (template.trainingRestrictions)
1318 entCategory = template.trainingRestrictions.category;
1319 else if (template.buildRestrictions)
1320 entCategory = template.buildRestrictions.category;
1321 if (entCategory && playerState.entityLimits[entCategory] !== undefined)
1323 r.entLimit = playerState.entityLimits[entCategory] || 0;
1324 r.entCount = playerState.entityCounts[entCategory] || 0;
1325 r.entLimitChangers = playerState.entityLimitChangers[entCategory];
1326 r.canBeAddedCount = Math.max(r.entLimit - r.entCount, 0);
1331 // Add the unit shown at position to the training queue for all entities in the selection
1332 function addTrainingByPosition(position)
1334 var simState = GetSimState();
1335 var playerState = simState.players[Engine.GetPlayerID()];
1336 var selection = g_Selection.toList();
1338 if (!playerState || !selection.length)
1341 var trainableEnts = getAllTrainableEntitiesFromSelection();
1343 // Check if the position is valid
1344 if (!trainableEnts.length || trainableEnts.length <= position)
1347 var entToTrain = trainableEnts[position];
1349 addTrainingToQueue(selection, entToTrain, playerState);
1353 // Called by GUI when user clicks training button
1354 function addTrainingToQueue(selection, trainEntType, playerState)
1356 // Create list of buildings which can train trainEntType
1357 var appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
1359 // Check trainEntType entity limit and count
1360 var limits = getEntityLimitAndCount(playerState, trainEntType);
1362 // Batch training possible if we can train at least 2 units
1363 var batchTrainingPossible = limits.canBeAddedCount == undefined || limits.canBeAddedCount > 1;
1365 var decrement = Engine.HotkeyIsPressed("selection.remove");
1367 var template = GetTemplateData(trainEntType);
1369 let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
1371 if (Engine.HotkeyIsPressed("session.batchtrain") && batchTrainingPossible)
1373 if (inputState == INPUT_BATCHTRAINING)
1375 // Check if we are training in the same building(s) as the last batch
1376 var sameEnts = false;
1377 if (batchTrainingEntities.length == selection.length)
1379 // NOTE: We just check if the arrays are the same and if the order is the same
1380 // If the order changed, we have a new selection and we should create a new batch.
1381 for (var i = 0; i < batchTrainingEntities.length; ++i)
1383 if (!(sameEnts = batchTrainingEntities[i] == selection[i]))
1387 // If we're already creating a batch of this unit (in the same building(s)), then just extend it
1388 // (if training limits allow)
1389 if (sameEnts && batchTrainingType == trainEntType)
1393 batchTrainingCount -= batchIncrementSize;
1394 if (batchTrainingCount <= 0)
1395 inputState = INPUT_NORMAL;
1397 else if (limits.canBeAddedCount == undefined ||
1398 limits.canBeAddedCount > batchTrainingCount * appropriateBuildings.length)
1400 if (Engine.GuiInterfaceCall("GetNeededResources", { "cost":
1401 multiplyEntityCosts(template, batchTrainingCount + batchIncrementSize) }))
1404 batchTrainingCount += batchIncrementSize;
1406 batchTrainingEntityAllowedCount = limits.canBeAddedCount;
1409 // Otherwise start a new one
1410 else if (!decrement)
1412 flushTrainingBatch();
1413 // fall through to create the new batch
1417 // Don't start a new batch if decrementing or unable to afford it.
1418 if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost":
1419 multiplyEntityCosts(template, batchIncrementSize) }))
1422 inputState = INPUT_BATCHTRAINING;
1423 batchTrainingEntities = selection;
1424 batchTrainingType = trainEntType;
1425 batchTrainingEntityAllowedCount = limits.canBeAddedCount;
1426 batchTrainingCount = batchIncrementSize;
1430 // Non-batched - just create a single entity in each building
1431 // (but no more than entity limit allows)
1432 var buildingsForTraining = appropriateBuildings;
1433 if (limits.entLimit)
1434 buildingsForTraining = buildingsForTraining.slice(0, limits.canBeAddedCount);
1435 Engine.PostNetworkCommand({
1437 "template": trainEntType,
1439 "entities": buildingsForTraining
1444 // Called by GUI when user clicks research button
1445 function addResearchToQueue(entity, researchType)
1447 Engine.PostNetworkCommand({ "type": "research", "entity": entity, "template": researchType });
1451 * Returns the number of units that will be present in a batch if the user clicks
1452 * the training button with shift down
1454 function getTrainingBatchStatus(playerState, trainEntType, selection)
1456 let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
1457 var appropriateBuildings = [];
1459 appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
1460 var nextBatchTrainingCount = 0;
1461 var currentBatchTrainingCount = 0;
1464 if (inputState == INPUT_BATCHTRAINING && batchTrainingType == trainEntType)
1466 nextBatchTrainingCount = batchTrainingCount;
1467 currentBatchTrainingCount = batchTrainingCount;
1469 "canBeAddedCount": batchTrainingEntityAllowedCount
1473 limits = getEntityLimitAndCount(playerState, trainEntType);
1475 // We need to calculate count after the next increment if it's possible
1476 if (limits.canBeAddedCount == undefined ||
1477 limits.canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length)
1478 nextBatchTrainingCount += batchIncrementSize;
1480 // If training limits don't allow us to train batchTrainingCount in each appropriate building
1481 // train as many full batches as we can and remainer in one more building.
1482 var buildingsCountToTrainFullBatch = appropriateBuildings.length;
1483 var remainderToTrain = 0;
1484 if (limits.canBeAddedCount !== undefined &&
1485 limits.canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length)
1487 buildingsCountToTrainFullBatch = Math.floor(limits.canBeAddedCount / nextBatchTrainingCount);
1488 remainderToTrain = limits.canBeAddedCount % nextBatchTrainingCount;
1491 return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain, currentBatchTrainingCount];
1494 // Called by GUI when user clicks production queue item
1495 function removeFromProductionQueue(entity, id)
1497 Engine.PostNetworkCommand({ "type": "stop-production", "entity": entity, "id": id });
1500 // Called by unit selection buttons
1501 function changePrimarySelectionGroup(templateName, deselectGroup)
1503 g_Selection.makePrimarySelection(templateName, Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup);
1506 function performCommand(entState, commandName)
1511 if (!controlsPlayer(entState.player) &&
1512 !(g_IsObserver && commandName == "focus-rally"))
1515 if (g_EntityCommands[commandName])
1516 g_EntityCommands[commandName].execute(entState);
1519 function performAllyCommand(entity, commandName)
1523 var entState = GetExtendedEntityState(entity);
1525 var playerState = GetSimState().players[Engine.GetPlayerID()];
1526 if (!playerState.isMutualAlly[entState.player] || g_IsObserver)
1529 if (g_AllyEntityCommands[commandName])
1530 g_AllyEntityCommands[commandName].execute(entState);
1533 function performFormation(entities, formationTemplate)
1536 Engine.PostNetworkCommand({
1537 "type": "formation",
1538 "entities": entities,
1539 "name": formationTemplate
1543 function performGroup(action, groupId)
1552 for (var ent in g_Groups.groups[groupId].ents)
1553 toSelect.push(+ent);
1555 if (action != "add")
1556 g_Selection.reset();
1558 g_Selection.addList(toSelect);
1560 if (action == "snap" && toSelect.length)
1562 let entState = GetEntityState(toSelect[0]);
1563 let position = entState.position;
1564 if (position && entState.visibility != "hidden")
1565 Engine.CameraMoveTo(position.x, position.z);
1570 g_Groups.groups[groupId].reset();
1572 if (action == "save")
1573 g_Groups.addEntities(groupId, g_Selection.toList());
1580 function performStance(entities, stanceName)
1583 Engine.PostNetworkCommand({
1585 "entities": entities,
1590 function lockGate(lock)
1592 var selection = g_Selection.toList();
1593 Engine.PostNetworkCommand({
1594 "type": "lock-gate",
1595 "entities": selection,
1600 function packUnit(pack)
1602 var selection = g_Selection.toList();
1603 Engine.PostNetworkCommand({
1605 "entities": selection,
1611 function cancelPackUnit(pack)
1613 var selection = g_Selection.toList();
1614 Engine.PostNetworkCommand({
1615 "type": "cancel-pack",
1616 "entities": selection,
1622 function upgradeEntity(Template)
1624 Engine.PostNetworkCommand({
1626 "entities": g_Selection.toList(),
1627 "template": Template,
1632 function cancelUpgradeEntity()
1634 Engine.PostNetworkCommand({
1635 "type": "cancel-upgrade",
1636 "entities": g_Selection.toList(),
1641 // Set the camera to follow the given unit
1642 function setCameraFollow(entity)
1644 // Follow the given entity if it's a unit
1647 var entState = GetEntityState(entity);
1648 if (entState && hasClass(entState, "Unit"))
1650 Engine.CameraFollow(entity);
1655 // Otherwise stop following
1656 Engine.CameraFollow(0);
1659 var lastIdleUnit = 0;
1660 var currIdleClassIndex = 0;
1661 var lastIdleClasses = [];
1663 function resetIdleUnit()
1666 currIdleClassIndex = 0;
1667 lastIdleClasses = [];
1670 function findIdleUnit(classes)
1672 var append = Engine.HotkeyIsPressed("selection.add");
1673 var selectall = Engine.HotkeyIsPressed("selection.offscreen");
1675 // Reset the last idle unit, etc., if the selection type has changed.
1676 if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i]))
1678 lastIdleClasses = classes;
1681 "viewedPlayer": g_ViewedPlayer,
1682 "excludeUnits": append ? g_Selection.toList() : [],
1683 // If the current idle class index is not 0, put the class at that index first.
1684 "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex))
1689 data.prevUnit = lastIdleUnit;
1692 var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
1693 if (!idleUnits.length)
1695 // TODO: display a message or play a sound to indicate no more idle units, or something
1696 // Reset for next cycle
1702 g_Selection.reset();
1703 g_Selection.addList(idleUnits);
1708 lastIdleUnit = idleUnits[0];
1709 var entityState = GetEntityState(lastIdleUnit);
1710 var position = entityState.position;
1712 Engine.CameraMoveTo(position.x, position.z);
1713 // Move the idle class index to the first class an idle unit was found for.
1714 var indexChange = data.idleClasses.findIndex(elem => hasClass(entityState, elem));
1715 currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length;
1718 function stopUnits(entities)
1720 Engine.PostNetworkCommand({ "type": "stop", "entities": entities, "queued": false });
1723 function unload(garrisonHolder, entities)
1725 if (Engine.HotkeyIsPressed("session.unloadtype"))
1726 Engine.PostNetworkCommand({ "type": "unload", "entities": entities, "garrisonHolder": garrisonHolder });
1728 Engine.PostNetworkCommand({ "type": "unload", "entities": [entities[0]], "garrisonHolder": garrisonHolder });
1731 function unloadTemplate(template)
1733 // Filter out all entities that aren't garrisonable.
1734 var garrisonHolders = g_Selection.toList().filter(e => {
1735 var state = GetEntityState(e);
1736 if (state && state.garrisonHolder)
1741 Engine.PostNetworkCommand({
1742 "type": "unload-template",
1743 "all": Engine.HotkeyIsPressed("session.unloadtype"),
1744 "template": template,
1745 "garrisonHolders": garrisonHolders
1749 function unloadSelection()
1753 for each (var ent in g_Selection.selected)
1755 var state = GetExtendedEntityState(ent);
1756 if (!state || !state.turretParent)
1760 parent = state.turretParent;
1763 else if (state.turretParent == parent)
1767 Engine.PostNetworkCommand({ "type": "unload", "entities":ents, "garrisonHolder": parent });
1770 function unloadAllByOwner()
1772 var garrisonHolders = g_Selection.toList().filter(e => {
1773 var state = GetEntityState(e);
1774 return state && state.garrisonHolder;
1776 Engine.PostNetworkCommand({ "type": "unload-all-by-owner", "garrisonHolders": garrisonHolders });
1779 function unloadAll()
1781 // Filter out all entities that aren't garrisonable.
1782 var garrisonHolders = g_Selection.toList().filter(e => {
1783 var state = GetEntityState(e);
1784 return state && state.garrisonHolder;
1787 Engine.PostNetworkCommand({ "type": "unload-all", "garrisonHolders": garrisonHolders });
1790 function backToWork()
1792 // Filter out all entities that can't go back to work.
1793 var workers = g_Selection.toList().filter(e => {
1794 var state = GetEntityState(e);
1795 return state && state.unitAI && state.unitAI.hasWorkOrders;
1798 Engine.PostNetworkCommand({ "type": "back-to-work", "entities": workers });
1801 function removeGuard()
1803 // Filter out all entities that are currently guarding/escorting.
1804 var entities = g_Selection.toList().filter(e => {
1805 var state = GetEntityState(e);
1806 return state && state.unitAI && state.unitAI.isGuarding;
1809 Engine.PostNetworkCommand({ "type": "remove-guard", "entities": entities });
1812 function raiseAlert()
1814 let entities = g_Selection.toList().filter(e => {
1815 let state = GetEntityState(e);
1816 return state && state.alertRaiser && !state.alertRaiser.hasRaisedAlert;
1819 Engine.PostNetworkCommand({ "type": "increase-alert-level", "entities": entities });
1822 function increaseAlertLevel()
1826 let entities = g_Selection.toList().filter(e => {
1827 let state = GetEntityState(e);
1828 return state && state.alertRaiser && state.alertRaiser.canIncreaseLevel;
1831 Engine.PostNetworkCommand({ "type": "increase-alert-level", "entities": entities });
1834 function endOfAlert()
1836 let entities = g_Selection.toList().filter(e => {
1837 let state = GetEntityState(e);
1838 return state && state.alertRaiser && state.alertRaiser.hasRaisedAlert;
1841 Engine.PostNetworkCommand({ "type": "alert-end", "entities": entities });
1844 function clearSelection()
1846 if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
1848 inputState = INPUT_NORMAL;
1849 placementSupport.Reset();
1852 g_Selection.reset();
1853 preSelectedAction = ACTION_NONE;