GUI cleanup.
[0ad.git] / binaries / data / mods / public / gui / session / input.js
blobc611630b46927c598d05d2f967d0cb387918dc1b
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;
38 var mouseX = 0;
39 var mouseY = 0;
40 var mouseIsOverObject = false;
42 // Number of pixels the mouse can move before the action is considered a drag
43 var maxDragDelta = 4;
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;
55 var prevHotkey = 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))
63         {
64                 let action = determineAction(mouseX, mouseY);
65                 if (action)
66                 {
67                         if (action.cursor)
68                         {
69                                 Engine.SetCursor(action.cursor);
70                                 cursorSet = true;
71                         }
72                         if (action.tooltip)
73                         {
74                                 tooltipSet = true;
75                                 informationTooltip.caption = action.tooltip;
76                                 informationTooltip.hidden = false;
77                         }
78                 }
79         }
81         if (!cursorSet)
82                 Engine.ResetCursor();
84         if (!tooltipSet)
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")
102         {
103                 if (placementSupport.template && placementSupport.position)
104                 {
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
111                         });
113                         // Show placement info tooltip if invalid position
114                         placementSupport.tooltipError = !result.success;
115                         placementSupport.tooltipMessage = "";
117                         if (!result.success)
118                         {
119                                 if (result.message && result.parameters)
120                                 {
121                                         var message = result.message;
122                                         if (result.translateMessage)
123                                                 if (result.pluralMessage)
124                                                         message = translatePlural(result.message, result.pluralMessage, result.pluralCount);
125                                                 else
126                                                         message = translate(message);
127                                         var parameters = result.parameters;
128                                         if (result.translateParameters)
129                                                 translateObjectKeys(parameters, result.translateParameters);
130                                         placementSupport.tooltipMessage = sprintf(message, parameters);
131                                 }
132                                 return false;
133                         }
135                         if (placementSupport.attack && placementSupport.attack.Ranged)
136                         {
137                                 // building can be placed here, and has an attack
138                                 // show the range advantage in the tooltip
139                                 var cmd = {
140                                         "x": placementSupport.position.x,
141                                         "z": placementSupport.position.z,
142                                         "range": placementSupport.attack.Ranged.maxRange,
143                                         "elevationBonus": placementSupport.attack.Ranged.elevationBonus,
144                                 };
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 });
149                         }
150                         return true;
151                 }
152         }
153         else if (placementSupport.mode === "wall")
154         {
155                 if (placementSupport.wallSet && placementSupport.position)
156                 {
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
163                         );
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
170                         });
171                 }
172         }
174         return false;
177 function findGatherType(gatherer, supply)
179         if (!("resourceGatherRates" in gatherer) || !gatherer.resourceGatherRates || !supply)
180                 return undefined;
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;
185         return undefined;
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]);
195         if (!entState)
196                 return { "possible": false };
198         if (!target) // TODO move these non-target actions to an object like unit_actions.js
199         {
200                 if (action == "set-rallypoint")
201                 {
202                         var cursor = "";
203                         var data = { "command": "walk" };
204                         if (Engine.HotkeyIsPressed("session.attackmove"))
205                         {
206                                 data.command = "attack-walk";
207                                 data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
208                                 cursor = "action-attack-move";
209                         }
210                         else if (Engine.HotkeyIsPressed("session.patrol"))
211                         {
212                                 data.command = "patrol";
213                                 data.targetClasses = { "attack": ["Unit"] };
214                                 cursor = "action-patrol";
215                         }
216                         return { "possible": true, "data": data, "cursor": cursor };
217                 }
219                 return { "possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) > -1 };
220         }
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)
231         {
232                 var entState = GetExtendedEntityState(entityID);
233                 if (!entState)
234                         continue;
236                 if (unitActions[action] && unitActions[action].getActionInfo)
237                 {
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
240                                 return r;
241                 }
242         }
243         return { "possible": false };
247  * Determine the context-sensitive action that should be performed when the mouse is at (x,y)
248  */
249 function determineAction(x, y, fromMinimap)
251         var selection = g_Selection.toList();
253         // No action if there's no selection
254         if (!selection.length)
255         {
256                 preSelectedAction = ACTION_NONE;
257                 return undefined;
258         }
260         // If the selection doesn't exist, no action
261         var entState = GetEntityState(selection[0]);
262         if (!entState)
263                 return undefined;
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;
269         });
271         if (!g_DevSettings.controlAll && !allOwnedByPlayer)
272                 return undefined;
274         var target = undefined;
275         if (!fromMinimap)
276         {
277                 var ent = Engine.PickEntityAtPoint(x, y);
278                 if (ent != INVALID_ENTITY)
279                         target = ent;
280         }
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)
290         {
291                 for (var action of actions)
292                         if (unitActions[action].preSelectedActionCheck)
293                         {
294                                 var r = unitActions[action].preSelectedActionCheck(target, selection);
295                                 if (r)
296                                         return r;
297                         }
299                 return { "type": "none", "cursor": "", "target": target };
300         }
302         for (var action of actions)
303                 if (unitActions[action].hotkeyActionCheck)
304                 {
305                         var r = unitActions[action].hotkeyActionCheck(target, selection);
306                         if (r)
307                                 return r;
308                 }
310         for (var action of actions)
311                 if (unitActions[action].actionCheck)
312                 {
313                         var r = unitActions[action].actionCheck(target, selection);
314                         if (r)
315                                 return r;
316                 }
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")
327         {
328                 error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'");
329                 return false;
330         }
332         if (!updateBuildingPlacementPreview())
333         {
334                 // invalid location - don't build it
335                 // TODO: play a sound?
336                 return false;
337         }
339         var selection = g_Selection.toList();
341         Engine.PostNetworkCommand({
342                 "type": "construct",
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,
349                 "autorepair": true,
350                 "autocontinue": true,
351                 "queued": queued
352         });
353         Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
355         if (!queued)
356                 placementSupport.Reset();
357         else
358                 placementSupport.RandomizeActorSeed();
360         return true;
363 function tryPlaceWall(queued)
365         if (placementSupport.mode !== "wall")
366         {
367                 error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'");
368                 return false;
369         }
371         var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
372         if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
373         {
374                 error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo));
375                 return false;
376         }
378         if (!wallPlacementInfo)
379                 return false;
381         var selection = g_Selection.toList();
382         var cmd = {
383                 "type": "construct-wall",
384                 "autorepair": true,
385                 "autocontinue": true,
386                 "queued": queued,
387                 "entities": selection,
388                 "wallSet": placementSupport.wallSet,
389                 "pieces": wallPlacementInfo.pieces,
390                 "startSnappedEntity": wallPlacementInfo.startSnappedEnt,
391                 "endSnappedEntity": wallPlacementInfo.endSnappedEnt,
392         };
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)
399         {
400                 if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
401                 {
402                         hasWallSegment = true;
403                         break;
404                 }
405         }
407         if (hasWallSegment)
408         {
409                 Engine.PostNetworkCommand(cmd);
410                 Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
411         }
413         return true;
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];
422         var x1 = ev.x;
423         var y1 = ev.y;
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
435 var unitFilters = {
436         "isUnit": entity => {
437                 var entState = GetEntityState(entity);
438                 return entState && hasClass(entState, "Unit");
439         },
440         "isDefensive": entity => {
441                 var entState = GetEntityState(entity);
442                 return entState && hasClass(entState, "Defensive");
443         },
444         "isMilitary": entity => {
445                 var entState = GetEntityState(entity);
446                 return entState &&
447                         g_MilitaryTypes.some(c => hasClass(entState, c));
448         },
449         "isIdle": entity => {
450                 var entState = GetEntityState(entity);
452                 return entState &&
453                         hasClass(entState, "Unit") &&
454                         entState.unitAI &&
455                         entState.unitAI.isIdle &&
456                         !hasClass(entState, "Domestic");
457         },
458         "isAnything": entity => {
459                 return true;
460         }
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)
467         // Default filters
468         var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything];
470         // Handle hotkeys
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)
478         {
479                 preferredEnts = ents.filter(filters[i]);
480                 if (preferredEnts.length)
481                         break;
482         }
483         return preferredEnts;
486 function handleInputBeforeGui(ev, hoveredObject)
488         // Capture mouse position so we can use it for displaying cursors,
489         // and key states
490         switch (ev.type)
491         {
492         case "mousebuttonup":
493         case "mousebuttondown":
494         case "mousemotion":
495                 mouseX = ev.x;
496                 mouseY = ev.y;
497                 break;
498         }
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))
506                 closeMenu();
508         // State-machine processing:
509         //
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
512         // returns true)
513         //
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
517         switch (inputState)
518         {
519         case INPUT_BANDBOXING:
520                 var bandbox = Engine.GetGUIObjectByName("bandbox");
521                 switch (ev.type)
522                 {
523                 case "mousemotion":
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);
530                         return false;
532                 case "mousebuttonup":
533                         if (ev.button == SDL_BUTTON_LEFT)
534                         {
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"))
545                                 {
546                                         g_Selection.addList(ents);
547                                 }
548                                 else if (Engine.HotkeyIsPressed("selection.remove"))
549                                 {
550                                         g_Selection.removeList(ents);
551                                 }
552                                 else
553                                 {
554                                         g_Selection.reset();
555                                         g_Selection.addList(ents);
556                                 }
558                                 inputState = INPUT_NORMAL;
559                                 return true;
560                         }
561                         else if (ev.button == SDL_BUTTON_RIGHT)
562                         {
563                                 // Cancel selection
564                                 bandbox.hidden = true;
566                                 g_Selection.setHighlightList([]);
568                                 inputState = INPUT_NORMAL;
569                                 return true;
570                         }
571                         break;
572                 }
573                 break;
575         case INPUT_BUILDING_CLICK:
576                 switch (ev.type)
577                 {
578                 case "mousemotion":
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)
585                         {
586                                 inputState = INPUT_BUILDING_DRAG;
587                                 return false;
588                         }
589                         break;
591                 case "mousebuttonup":
592                         if (ev.button == SDL_BUTTON_LEFT)
593                         {
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))
597                                 {
598                                         if (queued)
599                                                 inputState = INPUT_BUILDING_PLACEMENT;
600                                         else
601                                                 inputState = INPUT_NORMAL;
602                                 }
603                                 else
604                                 {
605                                         inputState = INPUT_BUILDING_PLACEMENT;
606                                 }
607                                 return true;
608                         }
609                         break;
611                 case "mousebuttondown":
612                         if (ev.button == SDL_BUTTON_RIGHT)
613                         {
614                                 // Cancel building
615                                 placementSupport.Reset();
616                                 inputState = INPUT_NORMAL;
617                                 return true;
618                         }
619                         break;
620                 }
621                 break;
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.
626                 switch (ev.type)
627                 {
628                 case "mousebuttonup":
629                         if (ev.button === SDL_BUTTON_LEFT)
630                         {
631                                 inputState = INPUT_BUILDING_WALL_PATHING;
632                                 return true;
633                         }
634                         break;
636                 case "mousebuttondown":
637                         if (ev.button == SDL_BUTTON_RIGHT)
638                         {
639                                 // Cancel building
640                                 placementSupport.Reset();
641                                 updateBuildingPlacementPreview();
643                                 inputState = INPUT_NORMAL;
644                                 return true;
645                         }
646                         break;
647                 }
648                 break;
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.
655                 switch (ev.type)
656                 {
657                         case "mousemotion":
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.
662                                 //
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
668                                 // points.
670                                 placementSupport.wallSnapEntitiesIncludeOffscreen = true;
671                                 var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
673                                 if (result && result.cost)
674                                 {
675                                         var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost });
676                                         placementSupport.tooltipMessage = [
677                                                 getEntityCostTooltip(result),
678                                                 getNeededResourcesTooltip(neededResources)
679                                         ].filter(tip => tip).join("\n");
680                                 }
682                                 break;
684                         case "mousebuttondown":
685                                 if (ev.button == SDL_BUTTON_LEFT)
686                                 {
687                                         var queued = Engine.HotkeyIsPressed("session.queue");
688                                         if (tryPlaceWall(queued))
689                                         {
690                                                 if (queued)
691                                                 {
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;
697                                                 }
698                                                 else
699                                                 {
700                                                         placementSupport.Reset();
701                                                         inputState = INPUT_NORMAL;
702                                                 }
703                                         }
704                                         else
705                                                 placementSupport.tooltipMessage = translate("Cannot build wall here!");
707                                         updateBuildingPlacementPreview();
708                                         return true;
709                                 }
710                                 else if (ev.button == SDL_BUTTON_RIGHT)
711                                 {
712                                         // reset to normal input mode
713                                         placementSupport.Reset();
714                                         updateBuildingPlacementPreview();
716                                         inputState = INPUT_NORMAL;
717                                         return true;
718                                 }
719                                 break;
720                 }
721                 break;
723         case INPUT_BUILDING_DRAG:
724                 switch (ev.type)
725                 {
726                 case "mousemotion":
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)
731                         {
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);
735                         }
736                         else
737                         {
738                                 // If the mouse is near the center, snap back to the default orientation
739                                 placementSupport.SetDefaultAngle();
740                         }
742                         var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
743                                 "template": placementSupport.template,
744                                 "x": placementSupport.position.x,
745                                 "z": placementSupport.position.z
746                         });
747                         if (snapData)
748                         {
749                                 placementSupport.angle = snapData.angle;
750                                 placementSupport.position.x = snapData.x;
751                                 placementSupport.position.z = snapData.z;
752                         }
754                         updateBuildingPlacementPreview();
755                         break;
757                 case "mousebuttonup":
758                         if (ev.button == SDL_BUTTON_LEFT)
759                         {
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))
763                                 {
764                                         if (queued)
765                                                 inputState = INPUT_BUILDING_PLACEMENT;
766                                         else
767                                                 inputState = INPUT_NORMAL;
768                                 }
769                                 else
770                                 {
771                                         inputState = INPUT_BUILDING_PLACEMENT;
772                                 }
773                                 return true;
774                         }
775                         break;
777                 case "mousebuttondown":
778                         if (ev.button == SDL_BUTTON_RIGHT)
779                         {
780                                 // Cancel building
781                                 placementSupport.Reset();
782                                 inputState = INPUT_NORMAL;
783                                 return true;
784                         }
785                         break;
786                 }
787                 break;
789         case INPUT_MASSTRIBUTING:
790                 if (ev.type == "hotkeyup" && ev.hotkey == "session.masstribute")
791                 {
792                         g_FlushTributing();
793                         inputState = INPUT_NORMAL;
794                 }
795                 break;
797         case INPUT_BATCHTRAINING:
798                 if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain")
799                 {
800                         flushTrainingBatch();
801                         inputState = INPUT_NORMAL;
802                 }
803                 break;
804         }
806         return false;
809 function handleInputAfterGui(ev)
811         if (ev.hotkey === undefined)
812                 ev.hotkey = null;
814         // Handle the time-warp testing features, restricted to single-player
815         if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked)
816         {
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();
823         }
825         if (ev.hotkey == "session.showstatusbars")
826         {
827                 g_ShowAllStatusBars = (ev.type == "hotkeydown");
828                 recalculateStatusBarDisplay();
829         }
830         else if (ev.hotkey == "session.highlightguarding")
831         {
832                 g_ShowGuarding = (ev.type == "hotkeydown");
833                 updateAdditionalHighlight();
834         }
835         else if (ev.hotkey == "session.highlightguarded")
836         {
837                 g_ShowGuarded = (ev.type == "hotkeydown");
838                 updateAdditionalHighlight();
839         }
841         // State-machine processing:
843         switch (inputState)
844         {
845         case INPUT_NORMAL:
846                 switch (ev.type)
847                 {
848                 case "mousemotion":
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]);
853                         else
854                                 g_Selection.setHighlightList([]);
856                         return false;
858                 case "mousebuttondown":
859                         if (ev.button == SDL_BUTTON_LEFT)
860                         {
861                                 dragStart = [ ev.x, ev.y ];
862                                 inputState = INPUT_SELECTING;
863                                 return true;
864                         }
865                         else if (ev.button == SDL_BUTTON_RIGHT)
866                         {
867                                 var action = determineAction(ev.x, ev.y);
868                                 if (!action)
869                                         break;
870                                 return doAction(action, ev);
871                         }
872                         break;
874                 case "hotkeydown":
875                                 if (ev.hotkey.indexOf("selection.group.") == 0)
876                                 {
877                                         var now = new Date();
878                                         if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey))
879                                         {
880                                                 if (ev.hotkey.indexOf("selection.group.select.") == 0)
881                                                 {
882                                                         var sptr = ev.hotkey.split(".");
883                                                         performGroup("snap", sptr[3]);
884                                                 }
885                                         }
886                                         else
887                                         {
888                                                 var sptr = ev.hotkey.split(".");
889                                                 performGroup(sptr[2], sptr[3]);
891                                                 doublePressTimer = now.getTime();
892                                                 prevHotkey = ev.hotkey;
893                                         }
894                                 }
895                                 break;
896                 }
897                 break;
899         case INPUT_PRESELECTEDACTION:
900                 switch (ev.type)
901                 {
902                 case "mousemotion":
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]);
907                         else
908                                 g_Selection.setHighlightList([]);
910                         return false;
912                 case "mousebuttondown":
913                         if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
914                         {
915                                 var action = determineAction(ev.x, ev.y);
916                                 if (!action)
917                                         break;
918                                 if (!Engine.HotkeyIsPressed("session.queue"))
919                                 {
920                                         preSelectedAction = ACTION_NONE;
921                                         inputState = INPUT_NORMAL;
922                                 }
923                                 return doAction(action, ev);
924                         }
925                         else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
926                         {
927                                 preSelectedAction = ACTION_NONE;
928                                 inputState = INPUT_NORMAL;
929                                 break;
930                         }
931                         // else
932                 default:
933                         // Slight hack: If selection is empty, reset the input state
934                         if (g_Selection.toList().length == 0)
935                         {
936                                 preSelectedAction = ACTION_NONE;
937                                 inputState = INPUT_NORMAL;
938                                 break;
939                         }
940                 }
941                 break;
943         case INPUT_SELECTING:
944                 switch (ev.type)
945                 {
946                 case "mousemotion":
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)
952                         {
953                                 inputState = INPUT_BANDBOXING;
954                                 return false;
955                         }
957                         var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
958                         if (ent != INVALID_ENTITY)
959                                 g_Selection.setHighlightList([ent]);
960                         else
961                                 g_Selection.setHighlightList([]);
962                         return false;
964                 case "mousebuttonup":
965                         if (ev.button == SDL_BUTTON_LEFT)
966                         {
967                                 var ents = [];
968                                 var selectedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
969                                 if (selectedEntity == INVALID_ENTITY)
970                                 {
971                                         if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
972                                         {
973                                                 g_Selection.reset();
974                                                 resetIdleUnit();
975                                         }
976                                         inputState = INPUT_NORMAL;
977                                         return true;
978                                 }
980                                 var now = new Date();
982                                 // If camera following and we select different unit, stop
983                                 if (Engine.GetFollowedEntity() != selectedEntity)
984                                 {
985                                         Engine.CameraFollow(0);
986                                 }
988                                 if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity))
989                                 {
990                                         // Double click or triple click has occurred
991                                         var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
992                                         var matchRank = true;
993                                         var templateToMatch;
995                                         // Check for double click or triple click
996                                         if (!doubleClicked)
997                                         {
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)
1002                                                 {
1003                                                         matchRank = false;
1004                                                 }
1005                                                 else
1006                                                 {       // No selection group name defined, so fall back to exact match
1007                                                         templateToMatch = GetEntityState(selectedEntity).template;
1008                                                 }
1010                                                 doubleClicked = true;
1011                                                 // Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click
1012                                                 doubleClickTimer = now.getTime();
1013                                         }
1014                                         else
1015                                         {
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;
1019                                         }
1021                                         // TODO: Should we handle "control all units" here as well?
1022                                         ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
1023                                 }
1024                                 else
1025                                 {
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];
1033                                 }
1035                                 // Update the list of selected units
1036                                 if (Engine.HotkeyIsPressed("selection.add"))
1037                                 {
1038                                         g_Selection.addList(ents);
1039                                 }
1040                                 else if (Engine.HotkeyIsPressed("selection.remove"))
1041                                 {
1042                                         g_Selection.removeList(ents);
1043                                 }
1044                                 else
1045                                 {
1046                                         g_Selection.reset();
1047                                         g_Selection.addList(ents);
1048                                 }
1050                                 inputState = INPUT_NORMAL;
1051                                 return true;
1052                         }
1053                         break;
1054                 }
1055                 break;
1057         case INPUT_BUILDING_PLACEMENT:
1058                 switch (ev.type)
1059                 {
1060                 case "mousemotion":
1062                         placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
1064                         if (placementSupport.mode === "wall")
1065                         {
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;
1070                         }
1071                         else
1072                         {
1073                                 // cancel if not enough resources
1074                                 if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost }))
1075                                 {
1076                                         placementSupport.Reset();
1077                                         inputState = INPUT_NORMAL;
1078                                         return true;
1079                                 }
1081                                 var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
1082                                         "template": placementSupport.template,
1083                                         "x": placementSupport.position.x,
1084                                         "z": placementSupport.position.z,
1085                                 });
1086                                 if (snapData)
1087                                 {
1088                                         placementSupport.angle = snapData.angle;
1089                                         placementSupport.position.x = snapData.x;
1090                                         placementSupport.position.z = snapData.z;
1091                                 }
1092                         }
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)
1099                         {
1100                                 if (placementSupport.mode === "wall")
1101                                 {
1102                                         var validPlacement = updateBuildingPlacementPreview();
1103                                         if (validPlacement !== false)
1104                                                 inputState = INPUT_BUILDING_WALL_CLICK;
1105                                 }
1106                                 else
1107                                 {
1108                                         placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
1109                                         dragStart = [ ev.x, ev.y ];
1110                                         inputState = INPUT_BUILDING_CLICK;
1111                                 }
1112                                 return true;
1113                         }
1114                         else if (ev.button == SDL_BUTTON_RIGHT)
1115                         {
1116                                 // Cancel building
1117                                 placementSupport.Reset();
1118                                 inputState = INPUT_NORMAL;
1119                                 return true;
1120                         }
1121                         break;
1123                 case "hotkeydown":
1125                         var rotation_step = Math.PI / 12; // 24 clicks make a full rotation
1127                         switch (ev.hotkey)
1128                         {
1129                         case "session.rotate.cw":
1130                                 placementSupport.angle += rotation_step;
1131                                 updateBuildingPlacementPreview();
1132                                 break;
1133                         case "session.rotate.ccw":
1134                                 placementSupport.angle -= rotation_step;
1135                                 updateBuildingPlacementPreview();
1136                                 break;
1137                         }
1138                         break;
1140                 }
1141                 break;
1142         }
1143         return false;
1146 function doAction(action, ev)
1148         if (!controlsPlayer(g_ViewedPlayer))
1149                 return false;
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);
1162         return false;
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)
1171                 return false;
1173         var fromMinimap = true;
1174         var action = determineAction(undefined, undefined, fromMinimap);
1175         if (!action)
1176                 return false;
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);
1184         return false;
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)
1192                 return;
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)
1203         {
1204                 placementSupport.mode = "wall";
1205                 placementSupport.wallSet = templateData.wallSet;
1206                 inputState = INPUT_BUILDING_PLACEMENT;
1207         }
1208         else
1209         {
1210                 placementSupport.mode = "building";
1211                 placementSupport.template = buildTemplate;
1212                 inputState = INPUT_BUILDING_PLACEMENT;
1213         }
1215         if (templateData.attack &&
1216                 templateData.attack.Ranged &&
1217                 templateData.attack.Ranged.maxRange)
1218         {
1219                 // add attack information to display a good tooltip
1220                 placementSupport.attack = templateData.attack;
1221         }
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 = [];
1228 var jumpCameraLast;
1229 var jumpCameraDistanceThreshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
1231 function jumpCamera(index)
1233         var position = jumpCameraPositions[index];
1234         if (position)
1235         {
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);
1240                 else
1241                 {
1242                         jumpCameraLast = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
1243                         Engine.CameraMoveTo(position.x, position.z);
1244                 }
1245         }
1248 function setJumpCamera(index)
1250         jumpCameraPositions[index] = {x: Engine.CameraGetX(), z: Engine.CameraGetZ()};
1253 // Batch training:
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)
1267         {
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({
1272                         "type": "train",
1273                         "entities": buildingsToTrainFullBatch,
1274                         "template": batchTrainingType,
1275                         "count": batchTrainingCount
1276                 });
1278                 // Train remainer in one more building
1279                 Engine.PostNetworkCommand({
1280                         "type": "train",
1281                         "entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ],
1282                         "template": batchTrainingType,
1283                         "count": batchTrainingEntityAllowedCount % batchTrainingCount
1284                 });
1285         }
1286         else
1287                 Engine.PostNetworkCommand({
1288                         "type": "train",
1289                         "entities": appropriateBuildings,
1290                         "template": batchTrainingType,
1291                         "count": batchTrainingCount
1292                 });
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;
1301                 return canTrain;
1302         });
1305 function getEntityLimitAndCount(playerState, entType)
1307         var r = {
1308                 "entLimit": undefined,
1309                 "entCount": undefined,
1310                 "entLimitChangers": undefined,
1311                 "canBeAddedCount": undefined
1312         };
1313         if (!playerState.entityLimits)
1314                 return r;
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)
1322         {
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);
1327         }
1328         return r;
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)
1339                 return;
1341         var trainableEnts = getAllTrainableEntitiesFromSelection();
1343         // Check if the position is valid
1344         if (!trainableEnts.length || trainableEnts.length <= position)
1345                 return;
1347         var entToTrain = trainableEnts[position];
1349         addTrainingToQueue(selection, entToTrain, playerState);
1350         return;
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");
1366         if (!decrement)
1367                 var template = GetTemplateData(trainEntType);
1369         let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
1371         if (Engine.HotkeyIsPressed("session.batchtrain") && batchTrainingPossible)
1372         {
1373                 if (inputState == INPUT_BATCHTRAINING)
1374                 {
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)
1378                         {
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)
1382                                 {
1383                                         if (!(sameEnts = batchTrainingEntities[i] == selection[i]))
1384                                                 break;
1385                                 }
1386                         }
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)
1390                         {
1391                                 if (decrement)
1392                                 {
1393                                         batchTrainingCount -= batchIncrementSize;
1394                                         if (batchTrainingCount <= 0)
1395                                                 inputState = INPUT_NORMAL;
1396                                 }
1397                                 else if (limits.canBeAddedCount == undefined ||
1398                                         limits.canBeAddedCount > batchTrainingCount * appropriateBuildings.length)
1399                                 {
1400                                         if (Engine.GuiInterfaceCall("GetNeededResources", { "cost":
1401                                                 multiplyEntityCosts(template, batchTrainingCount + batchIncrementSize) }))
1402                                                 return;
1404                                         batchTrainingCount += batchIncrementSize;
1405                                 }
1406                                 batchTrainingEntityAllowedCount = limits.canBeAddedCount;
1407                                 return;
1408                         }
1409                         // Otherwise start a new one
1410                         else if (!decrement)
1411                         {
1412                                 flushTrainingBatch();
1413                                 // fall through to create the new batch
1414                         }
1415                 }
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) }))
1420                         return;
1422                 inputState = INPUT_BATCHTRAINING;
1423                 batchTrainingEntities = selection;
1424                 batchTrainingType = trainEntType;
1425                 batchTrainingEntityAllowedCount = limits.canBeAddedCount;
1426                 batchTrainingCount = batchIncrementSize;
1427         }
1428         else
1429         {
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({
1436                         "type": "train",
1437                         "template": trainEntType,
1438                         "count": 1,
1439                         "entities": buildingsForTraining
1440                 });
1441         }
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
1453  */
1454 function getTrainingBatchStatus(playerState, trainEntType, selection)
1456         let batchIncrementSize = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
1457         var appropriateBuildings = [];
1458         if (selection)
1459                 appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
1460         var nextBatchTrainingCount = 0;
1461         var currentBatchTrainingCount = 0;
1463         var limits;
1464         if (inputState == INPUT_BATCHTRAINING && batchTrainingType == trainEntType)
1465         {
1466                 nextBatchTrainingCount = batchTrainingCount;
1467                 currentBatchTrainingCount = batchTrainingCount;
1468                 limits = {
1469                         "canBeAddedCount": batchTrainingEntityAllowedCount
1470                 };
1471         }
1472         else
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)
1486         {
1487                 buildingsCountToTrainFullBatch = Math.floor(limits.canBeAddedCount / nextBatchTrainingCount);
1488                 remainderToTrain = limits.canBeAddedCount % nextBatchTrainingCount;
1489         }
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)
1508         if (!entState)
1509                 return;
1511         if (!controlsPlayer(entState.player) &&
1512             !(g_IsObserver && commandName == "focus-rally"))
1513                 return;
1515         if (g_EntityCommands[commandName])
1516                 g_EntityCommands[commandName].execute(entState);
1519 function performAllyCommand(entity, commandName)
1521         if (!entity)
1522                 return;
1523         var entState = GetExtendedEntityState(entity);
1525         var playerState = GetSimState().players[Engine.GetPlayerID()];
1526         if (!playerState.isMutualAlly[entState.player] || g_IsObserver)
1527                 return;
1529         if (g_AllyEntityCommands[commandName])
1530                 g_AllyEntityCommands[commandName].execute(entState);
1533 function performFormation(entities, formationTemplate)
1535         if (entities)
1536                 Engine.PostNetworkCommand({
1537                         "type": "formation",
1538                         "entities": entities,
1539                         "name": formationTemplate
1540                 });
1543 function performGroup(action, groupId)
1545         switch (action)
1546         {
1547         case "snap":
1548         case "select":
1549         case "add":
1550                 var toSelect = [];
1551                 g_Groups.update();
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)
1561                 {
1562                         let entState = GetEntityState(toSelect[0]);
1563                         let position = entState.position;
1564                         if (position && entState.visibility != "hidden")
1565                                 Engine.CameraMoveTo(position.x, position.z);
1566                 }
1567                 break;
1568         case "save":
1569         case "breakUp":
1570                 g_Groups.groups[groupId].reset();
1572                 if (action == "save")
1573                         g_Groups.addEntities(groupId, g_Selection.toList());
1575                 updateGroups();
1576                 break;
1577         }
1580 function performStance(entities, stanceName)
1582         if (entities)
1583                 Engine.PostNetworkCommand({
1584                         "type": "stance",
1585                         "entities": entities,
1586                         "name": stanceName
1587                 });
1590 function lockGate(lock)
1592         var selection = g_Selection.toList();
1593         Engine.PostNetworkCommand({
1594                 "type": "lock-gate",
1595                 "entities": selection,
1596                 "lock": lock,
1597         });
1600 function packUnit(pack)
1602         var selection = g_Selection.toList();
1603         Engine.PostNetworkCommand({
1604                 "type": "pack",
1605                 "entities": selection,
1606                 "pack": pack,
1607                 "queued": false
1608         });
1611 function cancelPackUnit(pack)
1613         var selection = g_Selection.toList();
1614         Engine.PostNetworkCommand({
1615                 "type": "cancel-pack",
1616                 "entities": selection,
1617                 "pack": pack,
1618                 "queued": false
1619         });
1622 function upgradeEntity(Template)
1624         Engine.PostNetworkCommand({
1625                 "type": "upgrade",
1626                 "entities": g_Selection.toList(),
1627                 "template": Template,
1628                 "queued": false
1629         });
1632 function cancelUpgradeEntity()
1634         Engine.PostNetworkCommand({
1635                 "type": "cancel-upgrade",
1636                 "entities": g_Selection.toList(),
1637                 "queued": false
1638         });
1641 // Set the camera to follow the given unit
1642 function setCameraFollow(entity)
1644         // Follow the given entity if it's a unit
1645         if (entity)
1646         {
1647                 var entState = GetEntityState(entity);
1648                 if (entState && hasClass(entState, "Unit"))
1649                 {
1650                         Engine.CameraFollow(entity);
1651                         return;
1652                 }
1653         }
1655         // Otherwise stop following
1656         Engine.CameraFollow(0);
1659 var lastIdleUnit = 0;
1660 var currIdleClassIndex = 0;
1661 var lastIdleClasses = [];
1663 function resetIdleUnit()
1665         lastIdleUnit = 0;
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]))
1677                 resetIdleUnit();
1678         lastIdleClasses = classes;
1680         var data = {
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))
1685         };
1686         if (!selectall)
1687         {
1688                 data.limit = 1;
1689                 data.prevUnit = lastIdleUnit;
1690         }
1692         var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
1693         if (!idleUnits.length)
1694         {
1695                 // TODO: display a message or play a sound to indicate no more idle units, or something
1696                 // Reset for next cycle
1697                 resetIdleUnit();
1698                 return;
1699         }
1701         if (!append)
1702                 g_Selection.reset();
1703         g_Selection.addList(idleUnits);
1705         if (selectall)
1706                 return;
1708         lastIdleUnit = idleUnits[0];
1709         var entityState = GetEntityState(lastIdleUnit);
1710         var position = entityState.position;
1711         if (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 });
1727         else
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)
1737                         return true;
1738                 return false;
1739         });
1741         Engine.PostNetworkCommand({
1742                 "type": "unload-template",
1743                 "all": Engine.HotkeyIsPressed("session.unloadtype"),
1744                 "template": template,
1745                 "garrisonHolders": garrisonHolders
1746         });
1749 function unloadSelection()
1751         var parent = 0;
1752         var ents = [];
1753         for each (var ent in g_Selection.selected)
1754         {
1755                 var state = GetExtendedEntityState(ent);
1756                 if (!state || !state.turretParent)
1757                         continue;
1758                 if (!parent)
1759                 {
1760                         parent = state.turretParent;
1761                         ents.push(ent);
1762                 }
1763                 else if (state.turretParent == parent)
1764                         ents.push(ent);
1765         }
1766         if (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;
1775         });
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;
1785         });
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;
1796         });
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;
1807         });
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;
1817         });
1819         Engine.PostNetworkCommand({ "type": "increase-alert-level", "entities": entities });
1822 function increaseAlertLevel()
1824         raiseAlert();
1826         let entities = g_Selection.toList().filter(e => {
1827                 let state = GetEntityState(e);
1828                 return state && state.alertRaiser && state.alertRaiser.canIncreaseLevel;
1829         });
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;
1839         });
1841         Engine.PostNetworkCommand({ "type": "alert-end", "entities": entities });
1844 function clearSelection()
1846         if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
1847         {
1848                 inputState = INPUT_NORMAL;
1849                 placementSupport.Reset();
1850         }
1851         else
1852                 g_Selection.reset();
1853         preSelectedAction = ACTION_NONE;