Cleanup of GetEntityState
[0ad.git] / binaries / data / mods / public / gui / session / unit_actions.js
blob6fd9afb474fe699aecf91a19cca9bd341088c930
1 /**
2  * Specifies which template should indicate the target location of a player command,
3  * given a command type.
4  */
5 var g_TargetMarker = {
6         "move": "special/target_marker"
7 };
9 /**
10  * Which enemy entity types will be attacked on sight when patroling.
11  */
12 var g_PatrolTargets = ["Unit"];
14 /**
15  * List of different actions units can execute,
16  * this is mostly used to determine which actions can be executed
17  *
18  * "execute" is meant to send the command to the engine
19  *
20  * The next functions will always return false
21  * in case you have to continue to seek
22  * (i.e. look at the next entity for getActionInfo, the next
23  * possible action for the actionCheck ...)
24  * They will return an object when the searching is finished
25  *
26  * "getActionInfo" is used to determine if the action is possible,
27  * and also give visual feedback to the user (tooltips, cursors, ...)
28  *
29  * "preSelectedActionCheck" is used to select actions when the gui buttons
30  * were used to set them, but still require a target (like the guard button)
31  *
32  * "hotkeyActionCheck" is used to check the possibility of actions when
33  * a hotkey is pressed
34  *
35  * "actionCheck" is used to check the possibilty of actions without specific
36  * command. For that, the specificness variable is used
37  *
38  * "specificness" is used to determine how specific an action is,
39  * The lower the number, the more specific an action is, and the bigger
40  * the chance of selecting that action when multiple actions are possible
41  */
42 var g_UnitActions =
44         "move":
45         {
46                 "execute": function(target, action, selection, queued)
47                 {
48                         Engine.PostNetworkCommand({
49                                 "type": "walk",
50                                 "entities": selection,
51                                 "x": target.x,
52                                 "z": target.z,
53                                 "queued": queued
54                         });
56                         DrawTargetMarker(target);
58                         Engine.GuiInterfaceCall("PlaySound", {
59                                 "name": "order_walk",
60                                 "entity": selection[0]
61                         });
63                         return true;
64                 },
65                 "getActionInfo": function(entState, targetState)
66                 {
67                         return { "possible": true };
68                 },
69                 "hotkeyActionCheck": function(target, selection)
70                 {
71                         if (!someUnitAI(selection) ||
72                             !Engine.HotkeyIsPressed("session.move") ||
73                             !getActionInfo("move", target, selection).possible)
74                                 return false;
76                         return { "type": "move" };
77                 },
78                 "actionCheck": function(target, selection)
79                 {
80                         if (!someUnitAI(selection) || !getActionInfo("move", target, selection).possible)
81                                 return false;
83                         return { "type": "move" };
84                 },
85                 "specificness": 12,
86         },
88         "attack-move":
89         {
90                 "execute": function(target, action, selection, queued)
91                 {
92                         let targetClasses;
93                         if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
94                                 targetClasses = { "attack": ["Unit"] };
95                         else
96                                 targetClasses = { "attack": ["Unit", "Structure"] };
98                         Engine.PostNetworkCommand({
99                                 "type": "attack-walk",
100                                 "entities": selection,
101                                 "x": target.x,
102                                 "z": target.z,
103                                 "targetClasses": targetClasses,
104                                 "queued": queued
105                         });
107                         DrawTargetMarker(target);
109                         Engine.GuiInterfaceCall("PlaySound", {
110                                 "name": "order_walk",
111                                 "entity": selection[0]
112                         });
114                         return true;
115                 },
116                 "getActionInfo": function(entState, targetState)
117                 {
118                         return { "possible": true };
119                 },
120                 "hotkeyActionCheck": function(target, selection)
121                 {
122                         if (!someUnitAI(selection) ||
123                             !Engine.HotkeyIsPressed("session.attackmove") ||
124                             !getActionInfo("attack-move", target, selection).possible)
125                                 return false;
127                         return {
128                                 "type": "attack-move",
129                                 "cursor": "action-attack-move"
130                         };
131                 },
132                 "specificness": 30,
133         },
135         "capture":
136         {
137                 "execute": function(target, action, selection, queued)
138                 {
139                         Engine.PostNetworkCommand({
140                                 "type": "attack",
141                                 "entities": selection,
142                                 "target": action.target,
143                                 "allowCapture": true,
144                                 "queued": queued
145                         });
147                         Engine.GuiInterfaceCall("PlaySound", {
148                                 "name": "order_attack",
149                                 "entity": selection[0]
150                         });
152                         return true;
153                 },
154                 "getActionInfo": function(entState, targetState)
155                 {
156                         if (!entState.attack || !targetState.hitpoints)
157                                 return false;
159                         return {
160                                 "possible": Engine.GuiInterfaceCall("CanAttack", {
161                                         "entity": entState.id,
162                                         "target": targetState.id,
163                                         "types": ["Capture"]
164                                 })
165                         };
166                 },
167                 "actionCheck": function(target, selection)
168                 {
169                         if (!getActionInfo("capture", target, selection).possible)
170                                 return false;
172                         return {
173                                 "type": "capture",
174                                 "cursor": "action-capture",
175                                 "target": target
176                         };
177                 },
178                 "specificness": 9,
179         },
181         "attack":
182         {
183                 "execute": function(target, action, selection, queued)
184                 {
185                         Engine.PostNetworkCommand({
186                                 "type": "attack",
187                                 "entities": selection,
188                                 "target": action.target,
189                                 "queued": queued,
190                                 "allowCapture": false
191                         });
193                         Engine.GuiInterfaceCall("PlaySound", {
194                                 "name": "order_attack",
195                                 "entity": selection[0]
196                         });
198                         return true;
199                 },
200                 "getActionInfo": function(entState, targetState)
201                 {
202                         if (!entState.attack || !targetState.hitpoints)
203                                 return false;
205                         return {
206                                 "possible": Engine.GuiInterfaceCall("CanAttack", {
207                                         "entity": entState.id,
208                                         "target": targetState.id,
209                                         "types": ["!Capture"]
210                                 })
211                         };
212                 },
213                 "hotkeyActionCheck": function(target, selection)
214                 {
215                         if (!Engine.HotkeyIsPressed("session.attack") ||
216                             !getActionInfo("attack", target, selection).possible)
217                                 return false;
219                         return {
220                                 "type": "attack",
221                                 "cursor": "action-attack",
222                                 "target": target
223                         };
224                 },
225                 "actionCheck": function(target, selection)
226                 {
227                         if (!getActionInfo("attack", target, selection).possible)
228                                 return false;
230                         return {
231                                 "type": "attack",
232                                 "cursor": "action-attack",
233                                 "target": target
234                         };
235                 },
236                 "specificness": 10,
237         },
239         "patrol":
240         {
241                 "execute": function(target, action, selection, queued)
242                 {
243                         Engine.PostNetworkCommand({
244                                 "type": "patrol",
245                                 "entities": selection,
246                                 "x": target.x,
247                                 "z": target.z,
248                                 "target": action.target,
249                                 "targetClasses": { "attack": g_PatrolTargets },
250                                 "queued": queued,
251                                 "allowCapture": false
252                         });
254                         DrawTargetMarker(target);
256                         Engine.GuiInterfaceCall("PlaySound", { "name": "order_patrol", "entity": selection[0] });
257                         return true;
258                 },
259                 "getActionInfo": function(entState, targetState)
260                 {
261                         if (!entState.unitAI || !entState.unitAI.canPatrol)
262                                 return false;
264                         return { "possible": true };
265                 },
266                 "hotkeyActionCheck": function(target, selection)
267                 {
268                         if (!someCanPatrol(selection) ||
269                             !Engine.HotkeyIsPressed("session.patrol") ||
270                             !getActionInfo("patrol", target, selection).possible)
271                                 return false;
272                         return {
273                                 "type": "patrol",
274                                 "cursor": "action-patrol",
275                                 "target": target
276                         };
277                 },
278                 "preSelectedActionCheck": function(target, selection)
279                 {
280                         if (preSelectedAction != ACTION_PATROL || !getActionInfo("patrol", target, selection).possible)
281                                 return false;
282                         return {
283                                 "type": "patrol",
284                                 "cursor": "action-patrol",
285                                 "target": target
286                         };
287                 },
288                 "specificness": 37,
289         },
291         "heal":
292         {
293                 "execute": function(target, action, selection, queued)
294                 {
295                         Engine.PostNetworkCommand({
296                                 "type": "heal",
297                                 "entities": selection,
298                                 "target": action.target,
299                                 "queued": queued
300                         });
302                         Engine.GuiInterfaceCall("PlaySound", {
303                                 "name": "order_heal",
304                                 "entity": selection[0]
305                         });
307                         return true;
308                 },
309                 "getActionInfo": function(entState, targetState)
310                 {
311                         if (!entState.heal ||
312                             !hasClass(targetState, "Unit") || !targetState.needsHeal ||
313                             !playerCheck(entState, targetState, ["Player", "Ally"]) ||
314                             entState.id == targetState.id) // Healers can't heal themselves.
315                                 return false;
317                         let unhealableClasses = entState.heal.unhealableClasses;
318                         if (MatchesClassList(targetState.identity.classes, unhealableClasses))
319                                 return false;
321                         let healableClasses = entState.heal.healableClasses;
322                         if (!MatchesClassList(targetState.identity.classes, healableClasses))
323                                 return false;
325                         return { "possible": true };
326                 },
327                 "actionCheck": function(target, selection)
328                 {
329                         if (!getActionInfo("heal", target, selection).possible)
330                                 return false;
332                         return {
333                                 "type": "heal",
334                                 "cursor": "action-heal",
335                                 "target": target
336                         };
337                 },
338                 "specificness": 7,
339         },
341         "repair":
342         {
343                 "execute": function(target, action, selection, queued)
344                 {
345                         Engine.PostNetworkCommand({
346                                 "type": "repair",
347                                 "entities": selection,
348                                 "target": action.target,
349                                 "autocontinue": true,
350                                 "queued": queued
351                         });
353                         Engine.GuiInterfaceCall("PlaySound", {
354                                 "name": "order_repair",
355                                 "entity": selection[0]
356                         });
358                         return true;
359                 },
360                 "getActionInfo": function(entState, targetState)
361                 {
362                         if (!entState.builder ||
363                             !targetState.needsRepair && !targetState.foundation ||
364                             !playerCheck(entState, targetState, ["Player", "Ally"]))
365                                 return false;
367                         return { "possible": true };
368                 },
369                 "preSelectedActionCheck": function(target, selection)
370                 {
371                         if (preSelectedAction != ACTION_REPAIR)
372                                 return false;
374                         if (getActionInfo("repair", target, selection).possible)
375                                 return {
376                                         "type": "repair",
377                                         "cursor": "action-repair",
378                                         "target": target
379                                 };
381                         return {
382                                 "type": "none",
383                                 "cursor": "action-repair-disabled",
384                                 "target": null
385                         };
386                 },
387                 "hotkeyActionCheck": function(target, selection)
388                 {
389                         if (!Engine.HotkeyIsPressed("session.repair") ||
390                             !getActionInfo("repair", target, selection).possible)
391                                 return false;
393                         return {
394                                 "type": "repair",
395                                 "cursor": "action-repair",
396                                 "target": target
397                         };
398                 },
399                 "actionCheck": function(target, selection)
400                 {
401                         if (!getActionInfo("repair", target, selection).possible)
402                                 return false;
404                         return {
405                                 "type": "repair",
406                                 "cursor": "action-repair",
407                                 "target": target
408                         };
409                 },
410                 "specificness": 11,
411         },
413         "gather":
414         {
415                 "execute": function(target, action, selection, queued)
416                 {
417                         Engine.PostNetworkCommand({
418                                 "type": "gather",
419                                 "entities": selection,
420                                 "target": action.target,
421                                 "queued": queued
422                         });
424                         Engine.GuiInterfaceCall("PlaySound", {
425                                 "name": "order_gather",
426                                 "entity": selection[0]
427                         });
429                         return true;
430                 },
431                 "getActionInfo": function(entState, targetState)
432                 {
433                         if (!targetState.resourceSupply)
434                                 return false;
436                         let resource = findGatherType(entState, targetState.resourceSupply);
437                         if (!resource)
438                                 return false;
440                         return {
441                                 "possible": true,
442                                 "cursor": "action-gather-" + resource
443                         };
444                 },
445                 "actionCheck": function(target, selection)
446                 {
447                         let actionInfo = getActionInfo("gather", target, selection);
449                         if (!actionInfo.possible)
450                                 return false;
452                         return {
453                                 "type": "gather",
454                                 "cursor": actionInfo.cursor,
455                                 "target": target
456                         };
457                 },
458                 "specificness": 1,
459         },
461         "returnresource":
462         {
463                 "execute": function(target, action, selection, queued)
464                 {
465                         Engine.PostNetworkCommand({
466                                 "type": "returnresource",
467                                 "entities": selection,
468                                 "target": action.target,
469                                 "queued": queued
470                         });
472                         Engine.GuiInterfaceCall("PlaySound", {
473                                 "name": "order_gather",
474                                 "entity": selection[0]
475                         });
477                         return true;
478                 },
479                 "getActionInfo": function(entState, targetState)
480                 {
481                         if (!targetState.resourceDropsite)
482                                 return false;
484                         let playerState = GetSimState().players[entState.player];
485                         if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
486                         {
487                                 if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
488                                         return false;
489                         }
490                         else if (!playerCheck(entState, targetState, ["Player"]))
491                                 return false;
493                         if (!entState.resourceCarrying || !entState.resourceCarrying.length)
494                                 return false;
496                         let carriedType = entState.resourceCarrying[0].type;
497                         if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
498                                 return false;
500                         return {
501                                 "possible": true,
502                                 "cursor": "action-return-" + carriedType
503                         };
504                 },
505                 "actionCheck": function(target, selection)
506                 {
507                         let actionInfo = getActionInfo("returnresource", target, selection);
508                         if (!actionInfo.possible)
509                                 return false;
511                         return {
512                                 "type": "returnresource",
513                                 "cursor": actionInfo.cursor,
514                                 "target": target
515                         };
516                 },
517                 "specificness": 2,
518         },
520         "setup-trade-route":
521         {
522                 "execute": function(target, action, selection, queued)
523                 {
524                         Engine.PostNetworkCommand({
525                                 "type": "setup-trade-route",
526                                 "entities": selection,
527                                 "target": action.target,
528                                 "source": null,
529                                 "route": null,
530                                 "queued": queued
531                         });
533                         Engine.GuiInterfaceCall("PlaySound", {
534                                 "name": "order_trade",
535                                 "entity": selection[0]
536                         });
538                         return true;
539                 },
540                 "getActionInfo": function(entState, targetState)
541                 {
542                         if (targetState.foundation || !entState.trader || !targetState.market ||
543                             playerCheck(entState, targetState, ["Enemy"]) ||
544                             !(targetState.market.land && hasClass(entState, "Organic") ||
545                               targetState.market.naval && hasClass(entState, "Ship")))
546                                 return false;
548                         let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
549                                 "trader": entState.id,
550                                 "target": targetState.id
551                         });
553                         if (!tradingDetails)
554                                 return false;
556                         let tooltip;
557                         switch (tradingDetails.type)
558                         {
559                         case "is first":
560                                 tooltip = translate("Origin trade market.") + "\n";
561                                 if (tradingDetails.hasBothMarkets)
562                                         tooltip += sprintf(translate("Gain: %(gain)s"), {
563                                                 "gain": getTradingTooltip(tradingDetails.gain)
564                                         });
565                                 else
566                                         tooltip += translate("Right-click on another market to set it as a destination trade market.");
567                                 break;
569                         case "is second":
570                                 tooltip = translate("Destination trade market.") + "\n" +
571                                         sprintf(translate("Gain: %(gain)s"), {
572                                                 "gain": getTradingTooltip(tradingDetails.gain)
573                                         });
574                                 break;
576                         case "set first":
577                                 tooltip = translate("Right-click to set as origin trade market");
578                                 break;
580                         case "set second":
581                                 if (tradingDetails.gain.traderGain == 0) // markets too close
582                                         return false;
584                                 tooltip = translate("Right-click to set as destination trade market.") + "\n" +
585                                         sprintf(translate("Gain: %(gain)s"), {
586                                                 "gain": getTradingTooltip(tradingDetails.gain)
587                                         });
589                                 break;
590                         }
592                         return {
593                                 "possible": true,
594                                 "tooltip": tooltip
595                         };
596                 },
597                 "actionCheck": function(target, selection)
598                 {
599                         let actionInfo = getActionInfo("setup-trade-route", target, selection);
600                         if (!actionInfo.possible)
601                                 return false;
603                         return {
604                                 "type": "setup-trade-route",
605                                 "cursor": "action-setup-trade-route",
606                                 "tooltip": actionInfo.tooltip,
607                                 "target": target
608                         };
609                 },
610                 "specificness": 0,
611         },
613         "garrison":
614         {
615                 "execute": function(target, action, selection, queued)
616                 {
617                         Engine.PostNetworkCommand({
618                                 "type": "garrison",
619                                 "entities": selection,
620                                 "target": action.target,
621                                 "queued": queued
622                         });
624                         Engine.GuiInterfaceCall("PlaySound", {
625                                 "name": "order_garrison",
626                                 "entity": selection[0]
627                         });
629                         return true;
630                 },
631                 "getActionInfo": function(entState, targetState)
632                 {
633                         if (!entState.canGarrison || !targetState.garrisonHolder ||
634                             !playerCheck(entState, targetState, ["Player", "MutualAlly"]))
635                                 return false;
637                         let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
638                                 "garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount,
639                                 "capacity": targetState.garrisonHolder.capacity
640                         });
642                         let extraCount = 0;
643                         if (entState.garrisonHolder)
644                                 extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
646                         if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
647                                 tooltip = coloredText(tooltip, "orange");
649                         if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
650                                 return false;
652                         return {
653                                 "possible": true,
654                                 "tooltip": tooltip
655                         };
656                 },
657                 "preSelectedActionCheck": function(target, selection)
658                 {
659                         if (preSelectedAction != ACTION_GARRISON)
660                                 return false;
662                         let actionInfo = getActionInfo("garrison", target, selection);
663                         if (!actionInfo.possible)
664                                 return {
665                                         "type": "none",
666                                         "cursor": "action-garrison-disabled",
667                                         "target": null
668                                 };
670                         return {
671                                 "type": "garrison",
672                                 "cursor": "action-garrison",
673                                 "tooltip": actionInfo.tooltip,
674                                 "target": target
675                         };
676                 },
677                 "hotkeyActionCheck": function(target, selection)
678                 {
679                         let actionInfo = getActionInfo("garrison", target, selection);
681                         if (!Engine.HotkeyIsPressed("session.garrison") || !actionInfo.possible)
682                                 return false;
684                         return {
685                                 "type": "garrison",
686                                 "cursor": "action-garrison",
687                                 "tooltip": actionInfo.tooltip,
688                                 "target": target
689                         };
690                 },
691                 "specificness": 20,
692         },
694         "guard":
695         {
696                 "execute": function(target, action, selection, queued)
697                 {
698                         Engine.PostNetworkCommand({
699                                 "type": "guard",
700                                 "entities": selection,
701                                 "target": action.target,
702                                 "queued": queued
703                         });
705                         Engine.GuiInterfaceCall("PlaySound", {
706                                 "name": "order_guard",
707                                 "entity": selection[0]
708                         });
710                         return true;
711                 },
712                 "getActionInfo": function(entState, targetState)
713                 {
714                         if (!targetState.guard ||
715                             !playerCheck(entState, targetState, ["Player", "Ally"]) ||
716                             !entState.unitAI || !entState.unitAI.canGuard ||
717                             targetState.unitAI && targetState.unitAI.isGuarding)
718                                 return false;
720                         return { "possible": true };
721                 },
722                 "preSelectedActionCheck": function(target, selection)
723                 {
724                         if (preSelectedAction != ACTION_GUARD)
725                                 return false;
727                         if (getActionInfo("guard", target, selection).possible)
728                                 return {
729                                         "type": "guard",
730                                         "cursor": "action-guard",
731                                         "target": target
732                                 };
734                         return {
735                                 "type": "none",
736                                 "cursor": "action-guard-disabled",
737                                 "target": null
738                         };
739                 },
740                 "hotkeyActionCheck": function(target, selection)
741                 {
742                         if (!Engine.HotkeyIsPressed("session.guard") ||
743                             !getActionInfo("guard", target, selection).possible)
744                                 return false;
746                         return {
747                                 "type": "guard",
748                                 "cursor": "action-guard",
749                                 "target": target
750                         };
751                 },
752                 "specificness": 40,
753         },
755         "remove-guard":
756         {
757                 "execute": function(target, action, selection, queued)
758                 {
759                         Engine.PostNetworkCommand({
760                                 "type": "remove-guard",
761                                 "entities": selection,
762                                 "target": action.target,
763                                 "queued": queued
764                         });
766                         Engine.GuiInterfaceCall("PlaySound", {
767                                 "name": "order_guard",
768                                 "entity": selection[0]
769                         });
771                         return true;
772                 },
773                 "hotkeyActionCheck": function(target, selection)
774                 {
775                         if (!Engine.HotkeyIsPressed("session.guard") ||
776                             !getActionInfo("remove-guard", target, selection).possible ||
777                             !someGuarding(selection))
778                                 return false;
780                         return {
781                                 "type": "remove-guard",
782                                 "cursor": "action-remove-guard"
783                         };
784                 },
786                 "specificness": 41,
787         },
789         "set-rallypoint":
790         {
791                 "execute": function(target, action, selection, queued)
792                 {
793                         // if there is a position set in the action then use this so that when setting a
794                         // rally point on an entity it is centered on that entity
795                         if (action.position)
796                                 target = action.position;
798                         Engine.PostNetworkCommand({
799                                 "type": "set-rallypoint",
800                                 "entities": selection,
801                                 "x": target.x,
802                                 "z": target.z,
803                                 "data": action.data,
804                                 "queued": queued
805                         });
807                         // Display rally point at the new coordinates, to avoid display lag
808                         Engine.GuiInterfaceCall("DisplayRallyPoint", {
809                                 "entities": selection,
810                                 "x": target.x,
811                                 "z": target.z,
812                                 "queued": queued
813                         });
815                         return true;
816                 },
817                 "getActionInfo": function(entState, targetState)
818                 {
819                         let tooltip;
820                         // default to walking there (or attack-walking if hotkey pressed)
821                         let data = { "command": "walk" };
822                         let cursor = "";
824                         if (Engine.HotkeyIsPressed("session.attackmove"))
825                         {
826                                 let targetClasses;
827                                 if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
828                                         targetClasses = { "attack": ["Unit"] };
829                                 else
830                                         targetClasses = { "attack": ["Unit", "Structure"] };
832                                 data.command = "attack-walk";
833                                 data.targetClasses = targetClasses;
834                                 cursor = "action-attack-move";
835                         }
837                         if (Engine.HotkeyIsPressed("session.repair") &&
838                             (targetState.needsRepair || targetState.foundation) &&
839                             playerCheck(entState, targetState, ["Player", "Ally"]))
840                         {
841                                 data.command = "repair";
842                                 data.target = targetState.id;
843                                 cursor = "action-repair";
844                         }
845                         else if (targetState.garrisonHolder &&
846                             playerCheck(entState, targetState, ["Player", "MutualAlly"]))
847                         {
848                                 data.command = "garrison";
849                                 data.target = targetState.id;
850                                 cursor = "action-garrison";
852                                 tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
853                                         "garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount,
854                                         "capacity": targetState.garrisonHolder.capacity
855                                 });
857                                 if (targetState.garrisonHolder.garrisonedEntitiesCount >=
858                                     targetState.garrisonHolder.capacity)
859                                         tooltip = coloredText(tooltip, "orange");
860                         }
861                         else if (targetState.resourceSupply)
862                         {
863                                 let resourceType = targetState.resourceSupply.type;
864                                 if (resourceType.generic == "treasure")
865                                         cursor = "action-gather-" + resourceType.generic;
866                                 else
867                                         cursor = "action-gather-" + resourceType.specific;
869                                 data.command = "gather-near-position";
870                                 data.resourceType = resourceType;
871                                 data.resourceTemplate = targetState.template;
872                                 if (!targetState.speed)
873                                 {
874                                         data.command = "gather";
875                                         data.target = targetState.id;
876                                 }
877                         }
878                         else if (entState.market && targetState.market &&
879                                  entState.id != targetState.id &&
880                                  (!entState.market.naval || targetState.market.naval) &&
881                                  !playerCheck(entState, targetState, ["Enemy"]))
882                         {
883                                 // Find a trader (if any) that this building can produce.
884                                 let trader;
885                                 if (entState.production && entState.production.entities.length)
886                                         for (let i = 0; i < entState.production.entities.length; ++i)
887                                                 if ((trader = GetTemplateData(entState.production.entities[i]).trader))
888                                                         break;
890                                 let traderData = {
891                                         "firstMarket": entState.id,
892                                         "secondMarket": targetState.id,
893                                         "template": trader
894                                 };
896                                 let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
897                                 if (gain && gain.traderGain)
898                                 {
899                                         data.command = "trade";
900                                         data.target = traderData.secondMarket;
901                                         data.source = traderData.firstMarket;
902                                         cursor = "action-setup-trade-route";
904                                         tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
905                                                 sprintf(
906                                                         trader ?
907                                                                 translate("Gain: %(gain)s") :
908                                                                 translate("Expected gain: %(gain)s"),
909                                                         { "gain": getTradingTooltip(gain) });
910                                 }
911                         }
912                         else if ((targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
913                         {
914                                 data.command = "repair";
915                                 data.target = targetState.id;
916                                 cursor = "action-repair";
917                         }
918                         else if (playerCheck(entState, targetState, ["Enemy"]))
919                         {
920                                 data.target = targetState.id;
921                                 data.command = "attack";
922                                 cursor = "action-attack";
923                         }
925                         // Don't allow the rally point to be set on any of the currently selected entities (used for unset)
926                         // except if the autorallypoint hotkey is pressed and the target can produce entities
927                         if (!Engine.HotkeyIsPressed("session.autorallypoint") ||
928                             !targetState.production ||
929                             !targetState.production.entities.length)
930                                 for (let ent in g_Selection.selected)
931                                         if (targetState.id == +ent)
932                                                 return false;
934                         return {
935                                 "possible": true,
936                                 "data": data,
937                                 "position": targetState.position,
938                                 "cursor": cursor,
939                                 "tooltip": tooltip
940                         };
942                 },
943                 "actionCheck": function(target, selection)
944                 {
945                         if (someUnitAI(selection) || !someRallyPoints(selection))
946                                 return false;
948                         let actionInfo = getActionInfo("set-rallypoint", target, selection);
949                         if (!actionInfo.possible)
950                                 return false;
952                         return {
953                                 "type": "set-rallypoint",
954                                 "cursor": actionInfo.cursor,
955                                 "data": actionInfo.data,
956                                 "tooltip": actionInfo.tooltip,
957                                 "position": actionInfo.position
958                         };
959                 },
960                 "specificness": 6,
961         },
963         "unset-rallypoint":
964         {
965                 "execute": function(target, action, selection, queued)
966                 {
967                         Engine.PostNetworkCommand({
968                                 "type": "unset-rallypoint",
969                                 "entities": selection
970                         });
972                         // Remove displayed rally point
973                         Engine.GuiInterfaceCall("DisplayRallyPoint", {
974                                 "entities": []
975                         });
977                         return true;
978                 },
979                 "getActionInfo": function(entState, targetState)
980                 {
981                         if (entState.id != targetState.id ||
982                             !entState.rallyPoint || !entState.rallyPoint.position)
983                                 return false;
985                         return { "possible": true };
986                 },
987                 "actionCheck": function(target, selection)
988                 {
989                         if (someUnitAI(selection) || !someRallyPoints(selection) ||
990                             !getActionInfo("unset-rallypoint", target, selection).possible)
991                                 return false;
993                         return {
994                                 "type": "unset-rallypoint",
995                                 "cursor": "action-unset-rally"
996                         };
997                 },
998                 "specificness": 11,
999         },
1001         "none":
1002         {
1003                 "execute": function(target, action, selection, queued)
1004                 {
1005                         return true;
1006                 },
1007                 "specificness": 100,
1008         },
1012  * Info and actions for the entity commands
1013  * Currently displayed in the bottom of the central panel
1014  */
1015 var g_EntityCommands =
1017         "unload-all": {
1018                 "getInfo": function(entStates)
1019                 {
1020                         let count = 0;
1021                         for (let entState of entStates)
1022                                 if (entState.garrisonHolder)
1023                                         count += entState.garrisonHolder.entities.length;
1025                         if (!count)
1026                                 return false;
1028                         return {
1029                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
1030                                            translate("Unload All."),
1031                                 "icon": "garrison-out.png",
1032                                 "count": count,
1033                         };
1034                 },
1035                 "execute": function()
1036                 {
1037                         unloadAll();
1038                 },
1039         },
1041         "delete": {
1042                 "getInfo": function(entStates)
1043                 {
1044                         return entStates.some(entState => !isUndeletable(entState)) ?
1045                                 {
1046                                         "tooltip":
1047                                                 colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
1048                                                 translate("Destroy the selected units or buildings.") + "\n" +
1049                                                 colorizeHotkey(
1050                                                         translate("Use %(hotkey)s to avoid the confirmation dialog."),
1051                                                         "session.noconfirmation"
1052                                                 ),
1053                                         "icon": "kill_small.png"
1054                                 } :
1055                                 {
1056                                         // Get all delete reasons and remove duplications
1057                                         "tooltip": entStates.map(entState => isUndeletable(entState))
1058                                                 .filter((reason, pos, self) =>
1059                                                         self.indexOf(reason) == pos && reason
1060                                                 ).join("\n"),
1061                                         "icon": "kill_small_disabled.png"
1062                                 };
1063                 },
1064                 "execute": function(entStates)
1065                 {
1066                         if (!entStates.length || entStates.every(entState => isUndeletable(entState)))
1067                                 return;
1069                         if (Engine.HotkeyIsPressed("session.noconfirmation"))
1070                                 Engine.PostNetworkCommand({
1071                                         "type": "delete-entities",
1072                                         "entities": entStates.map(entState => entState.id)
1073                                 });
1074                         else
1075                                 openDeleteDialog(entStates.map(entState => entState.id));
1076                 },
1077         },
1079         "stop": {
1080                 "getInfo": function(entStates)
1081                 {
1082                         if (entStates.every(entState => !entState.unitAI))
1083                                 return false;
1085                         return {
1086                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
1087                                            translate("Abort the current order."),
1088                                 "icon": "stop.png"
1089                         };
1090                 },
1091                 "execute": function(entStates)
1092                 {
1093                         if (entStates.length)
1094                                 stopUnits(entStates.map(entState => entState.id));
1095                 },
1096         },
1098         "garrison": {
1099                 "getInfo": function(entStates)
1100                 {
1101                         if (entStates.every(entState => !entState.unitAI || entState.turretParent))
1102                                 return false;
1104                         return {
1105                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
1106                                            translate("Order the selected units to garrison in a building or unit."),
1107                                 "icon": "garrison.png"
1108                         };
1109                 },
1110                 "execute": function()
1111                 {
1112                         inputState = INPUT_PRESELECTEDACTION;
1113                         preSelectedAction = ACTION_GARRISON;
1114                 },
1115         },
1117         "unload": {
1118                 "getInfo": function(entStates)
1119                 {
1120                         if (entStates.every(entState => {
1121                                 if (!entState.unitAI || !entState.turretParent)
1122                                         return true;
1123                                 let parent = GetEntityState(entState.turretParent);
1124                                 return !parent || !parent.garrisonHolder || parent.garrisonHolder.entities.indexOf(entState.id) == -1;
1125                         }))
1126                                 return false;
1128                         return {
1129                                 "tooltip": translate("Unload"),
1130                                 "icon": "garrison-out.png"
1131                         };
1132                 },
1133                 "execute": function()
1134                 {
1135                         unloadSelection();
1136                 },
1137         },
1139         "repair": {
1140                 "getInfo": function(entStates)
1141                 {
1142                         if (entStates.every(entState => !entState.builder))
1143                                 return false;
1145                         return {
1146                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
1147                                            translate("Order the selected units to repair a building or mechanical unit."),
1148                                 "icon": "repair.png"
1149                         };
1150                 },
1151                 "execute": function()
1152                 {
1153                         inputState = INPUT_PRESELECTEDACTION;
1154                         preSelectedAction = ACTION_REPAIR;
1155                 },
1156         },
1158         "focus-rally": {
1159                 "getInfo": function(entStates)
1160                 {
1161                         if (entStates.every(entState => !entState.rallyPoint))
1162                                 return false;
1164                         return {
1165                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
1166                                            translate("Focus on Rally Point."),
1167                                 "icon": "focus-rally.png"
1168                         };
1169                 },
1170                 "execute": function(entStates)
1171                 {
1172                         // TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
1173                         let focusTarget;
1174                         for (let entState of entStates)
1175                                 if (entState.rallyPoint && entState.rallyPoint.position)
1176                                 {
1177                                         focusTarget = entState.rallyPoint.position;
1178                                         break;
1179                                 }
1180                         if (!focusTarget)
1181                                 for (let entState of entStates)
1182                                         if (entState.position)
1183                                         {
1184                                                 focusTarget = entState.position;
1185                                                 break;
1186                                         }
1188                         if (focusTarget)
1189                                 Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
1190                 },
1191         },
1193         "back-to-work": {
1194                 "getInfo": function(entStates)
1195                 {
1196                         if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
1197                                 return false;
1199                         return {
1200                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
1201                                            translate("Back to Work"),
1202                                 "icon": "production.png"
1203                         };
1204                 },
1205                 "execute": function()
1206                 {
1207                         backToWork();
1208                 },
1209         },
1211         "add-guard": {
1212                 "getInfo": function(entStates)
1213                 {
1214                         if (entStates.every(entState =>
1215                                 !entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
1216                                 return false;
1218                         return {
1219                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
1220                                            translate("Order the selected units to guard a building or unit."),
1221                                 "icon": "add-guard.png"
1222                         };
1223                 },
1224                 "execute": function()
1225                 {
1226                         inputState = INPUT_PRESELECTEDACTION;
1227                         preSelectedAction = ACTION_GUARD;
1228                 },
1229         },
1231         "remove-guard": {
1232                 "getInfo": function(entStates)
1233                 {
1234                         if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
1235                                 return false;
1237                         return {
1238                                 "tooltip": translate("Remove guard"),
1239                                 "icon": "remove-guard.png"
1240                         };
1241                 },
1242                 "execute": function()
1243                 {
1244                         removeGuard();
1245                 },
1246         },
1248         "select-trading-goods": {
1249                 "getInfo": function(entStates)
1250                 {
1251                         if (entStates.every(entState => !entState.market))
1252                                 return false;
1254                         return {
1255                                 "tooltip": translate("Barter & Trade"),
1256                                 "icon": "economics.png"
1257                         };
1258                 },
1259                 "execute": function()
1260                 {
1261                         toggleTrade();
1262                 },
1263         },
1265         "patrol": {
1266                 "getInfo": function(entStates)
1267                 {
1268                         if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
1269                                 return false;
1271                         return {
1272                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
1273                                            translate("Patrol") + "\n" +
1274                                            translate("Attack all encountered enemy units while avoiding buildings."),
1275                                 "icon": "patrol.png"
1276                         };
1277                 },
1278                 "execute": function()
1279                 {
1280                         inputState = INPUT_PRESELECTEDACTION;
1281                         preSelectedAction = ACTION_PATROL;
1282                 },
1283         },
1285         "share-dropsite": {
1286                 "getInfo": function(entStates)
1287                 {
1288                         let sharableEntities = entStates.filter(
1289                                 entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
1290                         if (!sharableEntities.length)
1291                                 return false;
1293                         // Returns if none of the entities belong to a player with a mutual ally
1294                         if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
1295                                 (isAlly, playerId) => isAlly && playerId != entState.player)))
1296                                 return false;
1298                         return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
1299                                 {
1300                                         "tooltip": translate("Press to allow allies to use this dropsite"),
1301                                         "icon": "locked_small.png"
1302                                 } :
1303                                 {
1304                                         "tooltip": translate("Press to prevent allies from using this dropsite"),
1305                                         "icon": "unlocked_small.png"
1306                                 };
1307                 },
1308                 "execute": function(entStates)
1309                 {
1310                         let sharableEntities = entStates.filter(
1311                                 entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
1312                         Engine.PostNetworkCommand({
1313                                 "type": "set-dropsite-sharing",
1314                                 "entities": sharableEntities.map(entState => entState.id),
1315                                 "shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
1316                         });
1317                 },
1318         }
1321 var g_AllyEntityCommands =
1323         "unload-all": {
1324                 "getInfo": function(entState)
1325                 {
1326                         if (!entState.garrisonHolder)
1327                                 return false;
1329                         let player = Engine.GetPlayerID();
1331                         let count = 0;
1332                         for (let ent in g_Selection.selected)
1333                         {
1334                                 let selectedEntState = GetEntityState(+ent);
1335                                 if (!selectedEntState.garrisonHolder)
1336                                         continue;
1338                                 for (let entity of selectedEntState.garrisonHolder.entities)
1339                                 {
1340                                         let state = GetEntityState(entity);
1341                                         if (state.player == player)
1342                                                 ++count;
1343                                 }
1344                         }
1346                         return {
1347                                 "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
1348                                            translate("Unload All."),
1349                                 "icon": "garrison-out.png",
1350                                 "count": count,
1351                         };
1352                 },
1353                 "execute": function(entState)
1354                 {
1355                         unloadAll();
1356                 },
1357         },
1359         "share-dropsite": {
1360                 "getInfo": function(entState)
1361                 {
1362                         if (Engine.GetPlayerID() == -1 ||
1363                             !GetSimState().players[Engine.GetPlayerID()].hasSharedDropsites ||
1364                             !entState.resourceDropsite || !entState.resourceDropsite.sharable)
1365                                 return false;
1367                         if (entState.resourceDropsite.shared)
1368                                 return {
1369                                         "tooltip": translate("You are allowed to use this dropsite"),
1370                                         "icon": "unlocked_small.png"
1371                                 };
1373                         return {
1374                                 "tooltip": translate("The use of this dropsite is prohibited"),
1375                                 "icon": "locked_small.png"
1376                         };
1377                 },
1378                 "execute": function(entState)
1379                 {
1380                         // This command button is always disabled
1381                 },
1382         }
1385 function playerCheck(entState, targetState, validPlayers)
1387         let playerState = GetSimState().players[entState.player];
1388         for (let player of validPlayers)
1389                 if (player == "Gaia" && targetState.player == 0 ||
1390                     player == "Player" && targetState.player == entState.player ||
1391                     playerState["is" + player] && playerState["is" + player][targetState.player])
1392                         return true;
1394         return false;
1397 function hasClass(entState, className)
1399         // note: use the functions in globalscripts/Templates.js for more versatile matching
1400         return entState.identity && entState.identity.classes.indexOf(className) != -1;
1404  * Work out whether at least part of the selected entities have UnitAI.
1405  */
1406 function someUnitAI(entities)
1408         return entities.some(ent => {
1409                 let entState = GetEntityState(ent);
1410                 return entState && entState.unitAI;
1411         });
1414 function someRallyPoints(entities)
1416         return entities.some(ent => {
1417                 let entState = GetEntityState(ent);
1418                 return entState && entState.rallyPoint;
1419         });
1422 function someGuarding(entities)
1424         return entities.some(ent => {
1425                 let entState = GetEntityState(ent);
1426                 return entState && entState.unitAI && entState.unitAI.isGuarding;
1427         });
1430 function someCanPatrol(entities)
1432         return entities.some(ent => {
1433                 let entState = GetEntityState(ent);
1434                 return entState && entState.unitAI && entState.unitAI.canPatrol;
1435         });
1439  * Keep in sync with Commands.js.
1440  */
1441 function isUndeletable(entState)
1443         if (g_DevSettings.controlAll)
1444                 return false;
1446         if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
1447                 return translate("The entity has to be killed before it can be gathered from");
1449         if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
1450                 return translate("You cannot destroy this entity as you own less than half the capture points");
1452         if (!entState.identity.canDelete)
1453                 return translate("This entity is undeletable");
1455         return false;
1458 function DrawTargetMarker(target)
1460         Engine.GuiInterfaceCall("AddTargetMarker", {
1461                 "template": g_TargetMarker.move,
1462                 "x": target.x,
1463                 "z": target.z
1464         });
1467 function findGatherType(gatherer, supply)
1469         if (!gatherer.resourceGatherRates || !supply)
1470                 return undefined;
1472         if (gatherer.resourceGatherRates[supply.type.generic + "." + supply.type.specific])
1473                 return supply.type.specific;
1475         if (gatherer.resourceGatherRates[supply.type.generic])
1476                 return supply.type.generic;
1478         return undefined;
1481 function getActionInfo(action, target, selection)
1483         let simState = GetSimState();
1485         // If the selection doesn't exist, no action
1486         if (!GetEntityState(selection[0]))
1487                 return { "possible": false };
1489         if (!target) // TODO move these non-target actions to an object like unit_actions.js
1490         {
1491                 if (action == "set-rallypoint")
1492                 {
1493                         let cursor = "";
1494                         let data = { "command": "walk" };
1495                         if (Engine.HotkeyIsPressed("session.attackmove"))
1496                         {
1497                                 data.command = "attack-walk";
1498                                 data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ?
1499                                         { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
1500                                 cursor = "action-attack-move";
1501                         }
1502                         else if (Engine.HotkeyIsPressed("session.patrol"))
1503                         {
1504                                 data.command = "patrol";
1505                                 data.targetClasses = { "attack": g_PatrolTargets };
1506                                 cursor = "action-patrol";
1507                         }
1508                         return {
1509                                 "possible": true,
1510                                 "data": data,
1511                                 "cursor": cursor
1512                         };
1513                 }
1515                 return {
1516                         "possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) != -1
1517                 };
1518         }
1520         // Look at the first targeted entity
1521         // (TODO: maybe we eventually want to look at more, and be more context-sensitive?
1522         // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
1523         let targetState = GetEntityState(target);
1525         // Check if any entities in the selection can do some of the available actions with target
1526         for (let entityID of selection)
1527         {
1528                 let entState = GetEntityState(entityID);
1529                 if (!entState)
1530                         continue;
1532                 if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
1533                 {
1534                         let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
1535                         if (r && r.possible) // return true if it's possible for one of the entities
1536                                 return r;
1537                 }
1538         }
1539         return { "possible": false };