Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.xml (revision 23080)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.xml (nonexistent)
@@ -1,20 +0,0 @@
-
-
-
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.xml
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.js (revision 23080)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.js (nonexistent)
@@ -1,259 +0,0 @@
-function DeveloperOverlay(playerViewControl)
-{
- this.commandHeight = 20;
- this.displayState = false;
- this.timeWarp = false;
- this.controlAll = false;
- this.playerViewControl = playerViewControl;
- Engine.GetGUIObjectByName("devCommandsOverlay").onPress = this.toggle;
- this.layout();
-
- registerSimulationUpdateHandler(this.update.bind(this));
- registerEntitySelectionChangeHandler(this.update.bind(this));
-}
-
-DeveloperOverlay.prototype.getCommands = function() {
- return [
- {
- "label": translate("Control all units"),
- "onPress": checked => {
- Engine.PostNetworkCommand({
- "type": "control-all",
- "flag": checked
- });
- },
- "checked": () => this.controlAll,
- },
- {
- "label": translate("Change perspective"),
- "onPress": checked => {
- this.playerViewControl.setChangePerspective(checked);
- },
- },
- {
- "label": translate("Display selection state"),
- "onPress": checked => {
- this.displayState = checked;
- this.update();
- },
- },
- {
- "label": translate("Pathfinder overlay"),
- "onPress": checked => {
- Engine.GuiInterfaceCall("SetPathfinderDebugOverlay", checked);
- },
- },
- {
- "label": translate("Obstruction overlay"),
- "onPress": checked => {
- Engine.GuiInterfaceCall("SetObstructionDebugOverlay", checked);
- },
- },
- {
- "label": translate("Unit motion overlay"),
- "onPress": checked => {
- g_Selection.SetMotionDebugOverlay(checked);
- },
- },
- {
- "label": translate("Range overlay"),
- "onPress": checked => {
- Engine.GuiInterfaceCall("SetRangeDebugOverlay", checked);
- },
- },
- {
- "label": translate("Bounding box overlay"),
- "onPress": checked => {
- Engine.SetBoundingBoxDebugOverlay(checked);
- },
- },
- {
- "label": translate("Restrict camera"),
- "onPress": checked => {
- Engine.GameView_SetConstrainCameraEnabled(checked);
- },
- "checked": () => Engine.GameView_GetConstrainCameraEnabled(),
- },
- {
- "label": translate("Reveal map"),
- "onPress": checked => {
- Engine.PostNetworkCommand({
- "type": "reveal-map",
- "enable": checked
- });
- },
- "checked": () => Engine.GuiInterfaceCall("IsMapRevealed"),
- },
- {
- "label": translate("Enable time warp"),
- "onPress": checked => {
- this.timeWarp = checked;
- if (checked)
- messageBox(
- 500, 250,
- translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."),
- translate("Time warp mode"));
- Engine.EnableTimeWarpRecording(checked ? 10 : 0);
- },
- },
- {
- "label": translate("Promote selected units"),
- "onPress": checked => {
- Engine.PostNetworkCommand({
- "type": "promote",
- "entities": g_Selection.toList()
- });
- },
- },
- {
- "label": translate("Hierarchical pathfinder overlay"),
- "onPress": checked => {
- Engine.GuiInterfaceCall("SetPathfinderHierDebugOverlay", checked);
- },
- },
- {
- "label": translate("Enable culling"),
- "onPress": checked => {
- Engine.GameView_SetCullingEnabled(checked);
- },
- "checked": () => Engine.GameView_GetCullingEnabled(),
- },
- {
- "label": translate("Lock cull camera"),
- "onPress": checked => {
- Engine.GameView_SetLockCullCameraEnabled(checked);
- },
- "checked": () => Engine.GameView_GetLockCullCameraEnabled(),
- },
- {
- "label": translate("Display camera frustum"),
- "onPress": checked => {
- Engine.Renderer_SetDisplayFrustumEnabled(checked);
- },
- "checked": () => Engine.Renderer_GetDisplayFrustumEnabled(),
- },
- ];
-};
-
-DeveloperOverlay.prototype.layout = function()
-{
- for (let body of Engine.GetGUIObjectByName("dev_commands").children)
- body.hidden = true;
-
- let overlayHeight = 0;
- let commands = this.getCommands();
- for (let i = 0; i < commands.length; ++i)
- {
- let command = commands[i];
-
- let body = Engine.GetGUIObjectByName("dev_command[" + i + "]");
- let bodySize = body.size;
- bodySize.top = i * this.commandHeight;
- bodySize.bottom = bodySize.top + this.commandHeight;
- body.size = bodySize;
- body.hidden = false;
-
- let label = Engine.GetGUIObjectByName("dev_command_label[" + i + "]");
- label.caption = command.label;
-
- let checkbox = Engine.GetGUIObjectByName("dev_command_checkbox[" + i + "]");
- checkbox.onPress = function() {
- command.onPress(checkbox.checked);
- if (command.checked)
- checkbox.checked = command.checked();
- };
-
- overlayHeight += this.commandHeight;
- }
-
- let overlay = Engine.GetGUIObjectByName("devCommandsOverlay");
- let overlaySize = overlay.size;
- overlaySize.bottom = overlaySize.top + overlayHeight;
- overlay.size = overlaySize;
-
- this.updateValues();
-};
-
-DeveloperOverlay.prototype.updateValues = function()
-{
- this.getCommands().forEach((command, i) => {
- let checkbox = Engine.GetGUIObjectByName("dev_command_checkbox[" + i + "]");
- if (command.checked)
- checkbox.checked = command.checked();
- });
-};
-
-DeveloperOverlay.prototype.toggle = function()
-{
- if (!g_GameAttributes.settings.CheatsEnabled && !g_IsReplay)
- return;
-
- let overlay = Engine.GetGUIObjectByName("devCommandsOverlay");
- overlay.hidden = !overlay.hidden;
-
- let message = overlay.hidden ?
- markForTranslation("The Developer Overlay was closed.") :
- markForTranslation("The Developer Overlay was opened.");
-
- // Only players can send the simulation chat command
- if (Engine.GetPlayerID() == -1)
- g_Chat.submitChat(message);
- else
- Engine.PostNetworkCommand({
- "type": "aichat",
- "message": message,
- "translateMessage": true,
- "translateParameters": [],
- "parameters": {}
- });
-};
-
-DeveloperOverlay.prototype.update = function()
-{
- let playerState = g_SimState.players[g_ViewedPlayer];
- this.controlAll = playerState ? playerState.controlsAll : false;
-
- this.updateValues();
- this.updateEntityState();
-}
-
-DeveloperOverlay.prototype.updateEntityState = function()
-{
- let debug = Engine.GetGUIObjectByName("debugEntityState");
-
- if (!this.displayState)
- {
- debug.hidden = true;
- return;
- }
-
- debug.hidden = false;
-
- let conciseSimState = clone(GetSimState());
- conciseSimState.players = "<<>>";
- let text = "simulation: " + uneval(conciseSimState);
-
- let selection = g_Selection.toList();
- if (selection.length)
- {
- let entState = GetEntityState(selection[0]);
- if (entState)
- {
- let template = GetTemplateData(entState.template);
- text += "\n\nentity: {\n";
- for (let k in entState)
- text += " " + k + ":" + uneval(entState[k]) + "\n";
- text += "}\n\ntemplate: " + uneval(template);
- }
- }
-
- debug.caption = text.replace(/\[/g, "\\[");
-};
-
-DeveloperOverlay.prototype.isTimeWarpEnabled = function() {
- return this.timeWarp;
-};
-
-DeveloperOverlay.prototype.isControlAll = function() {
- return this.controlAll;
-};
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.js (revision 23081)
@@ -0,0 +1,68 @@
+/**
+ * This class stores the checkboxes that are part of the developer overlay.
+ * These checkboxes may own their own helper class instances, such as the EntityState or TimeWarp feature.
+ */
+class DeveloperOverlay
+{
+ constructor(playerViewControl, selection)
+ {
+ this.devCommandsOverlay = Engine.GetGUIObjectByName("devCommandsOverlay");
+ this.devCommandsOverlay.onPress = this.toggle.bind(this);
+
+ this.checkBoxes = this.getCheckboxNames().map((name, i) =>
+ new DeveloperOverlayCheckbox(
+ new DeveloperOverlayCheckboxes.prototype[name](playerViewControl, selection),
+ i));
+
+ this.resize();
+ }
+
+ /**
+ * Mods may overwrite this to change the order.
+ */
+ getCheckboxNames()
+ {
+ return Object.keys(DeveloperOverlayCheckboxes.prototype);
+ }
+
+ toggle()
+ {
+ if (g_IsNetworked && !g_GameAttributes.settings.CheatsEnabled)
+ return;
+
+ this.devCommandsOverlay.hidden = !this.devCommandsOverlay.hidden;
+ this.sendNotification();
+ this.checkBoxes.forEach(checkbox => {
+ checkbox.setHidden(this.devCommandsOverlay.hidden);
+ });
+ }
+
+ sendNotification()
+ {
+ let message = this.devCommandsOverlay.hidden ? this.CloseNotification : this.OpenNotification;
+
+ // Only players can send the simulation chat command
+ if (Engine.GetPlayerID() == -1)
+ g_Chat.submitChat(message);
+ else
+ Engine.PostNetworkCommand({
+ "type": "aichat",
+ "message": message,
+ "translateMessage": true,
+ "translateParameters": [],
+ "parameters": {}
+ });
+ }
+
+ resize()
+ {
+ let size = this.devCommandsOverlay.size;
+ size.bottom =
+ size.top +
+ this.checkBoxes.reduce((height, checkbox) => height + checkbox.getHeight(), 0);
+ this.devCommandsOverlay.size = size;
+ }
+}
+
+DeveloperOverlay.prototype.OpenNotification = markForTranslation("The Developer Overlay was opened.");
+DeveloperOverlay.prototype.CloseNotification = markForTranslation("The Developer Overlay was closed.");
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.xml (revision 23081)
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlay.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckbox.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckbox.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckbox.js (revision 23081)
@@ -0,0 +1,63 @@
+/**
+ * This class sets up a checkbox in the developer overlay and assigns its specific handler.
+ */
+class DeveloperOverlayCheckbox
+{
+ constructor(handler, i)
+ {
+ this.handler = handler;
+
+ this.label = Engine.GetGUIObjectByName("dev_command_label[" + i + "]");
+ this.label.caption = this.handler.label();
+
+ this.checkbox = Engine.GetGUIObjectByName("dev_command_checkbox[" + i + "]");
+ this.checkbox.onPress = this.onPress.bind(this);
+
+ this.body = Engine.GetGUIObjectByName("dev_command[" + i + "]");
+ this.resize(i);
+
+ this.updater = this.update.bind(this);
+
+ if (this.handler.checked)
+ registerPlayersInitHandler(this.updater);
+ }
+
+ onPress()
+ {
+ this.handler.onPress(this.checkbox.checked);
+
+ if (this.handler.checked)
+ this.update();
+ }
+
+ update()
+ {
+ this.checkbox.checked = this.handler.checked();
+ }
+
+ setHidden(hidden)
+ {
+ if (!this.handler.checked)
+ return;
+
+ if (hidden)
+ unregisterSimulationUpdateHandler(this.updater);
+ else
+ registerSimulationUpdateHandler(this.updater);
+ }
+
+ getHeight()
+ {
+ return this.body.size.bottom - this.body.size.top;
+ }
+
+ resize(i)
+ {
+ let size = this.body.size;
+ let height = size.bottom;
+ size.top = height * i;
+ size.bottom = height * (i + 1);
+ this.body.size = size;
+ this.body.hidden = false;
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckbox.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckboxes.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckboxes.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckboxes.js (revision 23081)
@@ -0,0 +1,277 @@
+/**
+ * This class stores the handlers for the individual checkboxes available in the developer overlay.
+ * Such a class must have label and onPress function.
+ * If the class has a checked property, then that will be called every simulation update to
+ * synchronize the state of the checkbox (only if the developer overaly is opened).
+ */
+class DeveloperOverlayCheckboxes
+{
+}
+
+DeveloperOverlayCheckboxes.prototype.ControlAll = class
+{
+ label()
+ {
+ return translate("Control all units");
+ }
+
+ onPress(checked)
+ {
+ Engine.PostNetworkCommand({
+ "type": "control-all",
+ "flag": checked
+ });
+ }
+
+ checked()
+ {
+ let playerState = g_SimState.players[g_ViewedPlayer];
+ return playerState ? playerState.controlsAll : false;
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.ChangePerspective = class
+{
+ constructor(playerViewControl)
+ {
+ this.playerViewControl = playerViewControl;
+ }
+
+ label()
+ {
+ return translate("Change perspective");
+ }
+
+ onPress(checked)
+ {
+ this.playerViewControl.setChangePerspective(checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.SelectionEntityState = class
+{
+ constructor(playerViewControl, selection)
+ {
+ this.developerOverlayEntityState = new DeveloperOverlayEntityState(selection);
+ }
+
+ label()
+ {
+ return translate("Display selection state");
+ }
+
+ onPress(checked)
+ {
+ this.developerOverlayEntityState.setEnabled(checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.PathfinderOverlay = class
+{
+ label()
+ {
+ return translate("Pathfinder overlay");
+ }
+
+ onPress(checked)
+ {
+ Engine.GuiInterfaceCall("SetPathfinderDebugOverlay", checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.HierarchicalPathfinderOverlay = class
+{
+ label()
+ {
+ return translate("Hierarchical pathfinder overlay");
+ }
+
+ onPress(checked)
+ {
+ Engine.GuiInterfaceCall("SetPathfinderHierDebugOverlay", checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.ObstructionOverlay = class
+{
+ label()
+ {
+ return translate("Obstruction overlay");
+ }
+
+ onPress(checked)
+ {
+ Engine.GuiInterfaceCall("SetObstructionDebugOverlay", checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.UnitMotionOverlay = class
+{
+ label()
+ {
+ return translate("Unit motion overlay");
+ }
+
+ onPress(checked)
+ {
+ g_Selection.SetMotionDebugOverlay(checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.RangeOverlay = class
+{
+ label()
+ {
+ return translate("Range overlay");
+ }
+
+ onPress(checked)
+ {
+ Engine.GuiInterfaceCall("SetRangeDebugOverlay", checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.BoundingBoxOverlay = class
+{
+ label()
+ {
+ return translate("Bounding box overlay");
+ }
+
+ onPress(checked)
+ {
+ Engine.SetBoundingBoxDebugOverlay(checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.RestrictCamera = class
+{
+ label()
+ {
+ return translate("Restrict camera");
+ }
+
+ onPress(checked)
+ {
+ Engine.GameView_SetConstrainCameraEnabled(checked);
+ }
+
+ checked()
+ {
+ return Engine.GameView_GetConstrainCameraEnabled();
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.RevealMap = class
+{
+ label()
+ {
+ return translate("Reveal map");
+ }
+
+ onPress(checked)
+ {
+ Engine.PostNetworkCommand({
+ "type": "reveal-map",
+ "enable": checked
+ });
+ }
+
+ checked()
+ {
+ return Engine.GuiInterfaceCall("IsMapRevealed");
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.EnableTimeWarp = class
+{
+ constructor()
+ {
+ this.timeWarp = new TimeWarp();
+ }
+
+ label()
+ {
+ return translate("Enable time warp");
+ }
+
+ onPress(checked)
+ {
+ this.timeWarp.setEnabled(checked);
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.PromoteSelectedUnits = class
+{
+ label()
+ {
+ return translate("Promote selected units");
+ }
+
+ onPress(checked)
+ {
+ Engine.PostNetworkCommand({
+ "type": "promote",
+ "entities": g_Selection.toList()
+ });
+ }
+
+ checked()
+ {
+ return false;
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.EnableCulling = class
+{
+ label()
+ {
+ return translate("Enable culling");
+ }
+
+ onPress(checked)
+ {
+ Engine.GameView_SetCullingEnabled(checked);
+ }
+
+ checked()
+ {
+ return Engine.GameView_GetCullingEnabled();
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.LockCullCamera = class
+{
+ label()
+ {
+ return translate("Lock cull camera");
+ }
+
+ onPress(checked)
+ {
+ Engine.GameView_SetLockCullCameraEnabled(checked);
+ }
+
+ checked()
+ {
+ return Engine.GameView_GetLockCullCameraEnabled();
+ }
+};
+
+DeveloperOverlayCheckboxes.prototype.DisplayCameraFrustum = class
+{
+ label()
+ {
+ return translate("Display camera frustum");
+ }
+
+ onPress(checked)
+ {
+ Engine.Renderer_SetDisplayFrustumEnabled(checked);
+ }
+
+ checked()
+ {
+ return Engine.Renderer_GetDisplayFrustumEnabled();
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayCheckboxes.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.js (revision 23081)
@@ -0,0 +1,51 @@
+/**
+ * This class manages the developer overlay which displays the state of the first selected entity.
+ */
+class DeveloperOverlayEntityState
+{
+ constructor(selection)
+ {
+ this.developerOverlayEntityState = Engine.GetGUIObjectByName("developerOverlayEntityState");
+ this.selection = selection;
+ this.updater = this.update.bind(this);
+ }
+
+ setEnabled(enabled)
+ {
+ this.developerOverlayEntityState.hidden = !enabled;
+
+ if (enabled)
+ {
+ registerSimulationUpdateHandler(this.updater);
+ registerEntitySelectionChangeHandler(this.updater);
+ }
+ else
+ {
+ unregisterSimulationUpdateHandler(this.updater);
+ unregisterEntitySelectionChangeHandler(this.updater);
+ }
+ }
+
+ update()
+ {
+ let simState = clone(g_SimState);
+ simState.players = "<<>>";
+ let text = "simulation: " + uneval(simState);
+
+ let selection = this.selection.toList();
+ if (selection.length)
+ {
+ let entState = GetEntityState(selection[0]);
+ if (entState)
+ {
+ let template = GetTemplateData(entState.template);
+ text += "\n\nentity: {\n";
+ for (let k in entState)
+ text += " " + k + ":" + uneval(entState[k]) + "\n";
+ text += "}\n\ntemplate: " + uneval(template);
+ }
+ }
+
+ this.developerOverlayEntityState.caption = text.replace(/\[/g, "\\[");
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.xml (revision 23081)
@@ -0,0 +1,9 @@
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/DeveloperOverlayEntityState.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.js (revision 23081)
@@ -0,0 +1,66 @@
+/**
+ * This class manages the timewarp hotkeys that allow jumping back in time and fast forwarding the game.
+ */
+class TimeWarp
+{
+ constructor()
+ {
+ this.enabled = false;
+
+ this.timewarpRewind = Engine.GetGUIObjectByName("timewarpRewind");
+ this.timewarpRewind.onPress = this.rewind.bind(this);
+
+ this.timewarpFastForward = Engine.GetGUIObjectByName("timewarpFastForward");
+ this.timewarpFastForward.onPress = this.fastForward.bind(this);
+ this.timewarpFastForward.onRelease = this.resetSpeed.bind(this);
+ }
+
+ setEnabled(enabled)
+ {
+ if (g_IsNetworked)
+ return;
+
+ this.enabled = enabled;
+
+ if (enabled)
+ messageBox(
+ 500, 250,
+ translate(this.Description),
+ translate(this.Title));
+
+ Engine.EnableTimeWarpRecording(enabled ? this.NumberTurns : 0);
+ }
+
+ rewind()
+ {
+ if (this.enabled)
+ Engine.RewindTimeWarp();
+ }
+
+ fastForward()
+ {
+ if (this.enabled)
+ Engine.SetSimRate(this.FastForwardSpeed);
+ }
+
+ resetSpeed()
+ {
+ if (this.enabled)
+ Engine.SetSimRate(1);
+ }
+}
+
+/**
+ * Number of turns between snapshots.
+ */
+TimeWarp.prototype.NumberTurns = 10;
+
+/**
+ * Gamespeed used while pressing the fast forward hotkey.
+ */
+TimeWarp.prototype.FastForwardSpeed = 20;
+
+TimeWarp.prototype.Title = markForTranslation("Time warp mode");
+
+TimeWarp.prototype.Description = markForTranslation(
+ "Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash.");
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.xml (revision 23081)
@@ -0,0 +1,5 @@
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay/TimeWarp.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 23080)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 23081)
@@ -1,1627 +1,1624 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
const SDLK_LEFTBRACKET = 91;
const SDLK_RIGHTBRACKET = 93;
const SDLK_RSHIFT = 303;
const SDLK_LSHIFT = 304;
const SDLK_RCTRL = 305;
const SDLK_LCTRL = 306;
const SDLK_RALT = 307;
const SDLK_LALT = 308;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too
const ACTION_NONE = 0;
const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
const ACTION_GUARD = 3;
const ACTION_PATROL = 4;
var preSelectedAction = ACTION_NONE;
const INPUT_NORMAL = 0;
const INPUT_SELECTING = 1;
const INPUT_BANDBOXING = 2;
const INPUT_BUILDING_PLACEMENT = 3;
const INPUT_BUILDING_CLICK = 4;
const INPUT_BUILDING_DRAG = 5;
const INPUT_BATCHTRAINING = 6;
const INPUT_PRESELECTEDACTION = 7;
const INPUT_BUILDING_WALL_CLICK = 8;
const INPUT_BUILDING_WALL_PATHING = 9;
const INPUT_UNIT_POSITION_START = 10;
const INPUT_UNIT_POSITION = 11;
var inputState = INPUT_NORMAL;
const INVALID_ENTITY = 0;
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
/**
* Containing the ingame position which span the line.
*/
var g_FreehandSelection_InputLine = [];
/**
* Minimum squared distance when a mouse move is called a drag.
*/
const g_FreehandSelection_ResolutionInputLineSquared = 1;
/**
* Minimum length a dragged line should have to use the freehand selection.
*/
const g_FreehandSelection_MinLengthOfLine = 8;
/**
* To start the freehandSelection function you need a minimum number of units.
* Minimum must be 2, for better performance you could set it higher.
*/
const g_FreehandSelection_MinNumberOfUnits = 2;
/**
* Number of pixels the mouse can move before the action is considered a drag.
*/
const g_MaxDragDelta = 4;
/**
* Used for remembering mouse coordinates at start of drag operations.
*/
var g_DragStart;
/**
* Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities.
* If any mousedown or mouseup of a sequence of clicks lands on a unit,
* that unit will be selected, which makes it easier to click on moving units.
*/
var clickedEntity = INVALID_ENTITY;
// Same double-click behaviour for hotkey presses
const doublePressTime = 500;
var doublePressTimer = 0;
var prevHotkey = 0;
function updateCursorAndTooltip()
{
var cursorSet = false;
var tooltipSet = false;
var informationTooltip = Engine.GetGUIObjectByName("informationTooltip");
if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION))
{
let action = determineAction(mouseX, mouseY);
if (action)
{
if (action.cursor)
{
Engine.SetCursor(action.cursor);
cursorSet = true;
}
if (action.tooltip)
{
tooltipSet = true;
informationTooltip.caption = action.tooltip;
informationTooltip.hidden = false;
}
}
}
if (!cursorSet)
Engine.ResetCursor();
if (!tooltipSet)
informationTooltip.hidden = true;
var placementTooltip = Engine.GetGUIObjectByName("placementTooltip");
if (placementSupport.tooltipMessage)
placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip";
placementTooltip.caption = placementSupport.tooltipMessage || "";
placementTooltip.hidden = !placementSupport.tooltipMessage;
}
function updateBuildingPlacementPreview()
{
// The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or
// in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to.
// See onSimulationUpdate in session.js.
if (placementSupport.mode === "building")
{
if (placementSupport.template && placementSupport.position)
{
var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed
});
// Show placement info tooltip if invalid position
placementSupport.tooltipError = !result.success;
placementSupport.tooltipMessage = "";
if (!result.success)
{
if (result.message && result.parameters)
{
var message = result.message;
if (result.translateMessage)
if (result.pluralMessage)
message = translatePlural(result.message, result.pluralMessage, result.pluralCount);
else
message = translate(message);
var parameters = result.parameters;
if (result.translateParameters)
translateObjectKeys(parameters, result.translateParameters);
placementSupport.tooltipMessage = sprintf(message, parameters);
}
return false;
}
if (placementSupport.attack && placementSupport.attack.Ranged)
{
// building can be placed here, and has an attack
// show the range advantage in the tooltip
var cmd = {
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"range": placementSupport.attack.Ranged.maxRange,
"elevationBonus": placementSupport.attack.Ranged.elevationBonus,
};
var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range);
var range = Math.round(cmd.range);
placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" +
sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange });
}
return true;
}
}
else if (placementSupport.mode === "wall")
{
if (placementSupport.wallSet && placementSupport.position)
{
// Fetch an updated list of snapping candidate entities
placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities(
placementSupport.wallSet.templates.tower,
placementSupport.wallSnapEntitiesIncludeOffscreen,
true, // require exact template match
true // include foundations
);
return Engine.GuiInterfaceCall("SetWallPlacementPreview", {
"wallSet": placementSupport.wallSet,
"start": placementSupport.position,
"end": placementSupport.wallEndPosition,
"snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment
});
}
}
return false;
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y, fromMinimap)
{
var selection = g_Selection.toList();
// No action if there's no selection
if (!selection.length)
{
preSelectedAction = ACTION_NONE;
return undefined;
}
// If the selection doesn't exist, no action
var entState = GetEntityState(selection[0]);
if (!entState)
return undefined;
// If the selection isn't friendly units, no action
- var allOwnedByPlayer = selection.every(ent => {
- var entState = GetEntityState(ent);
- return entState && entState.player == g_ViewedPlayer;
- });
-
- if (!g_DeveloperOverlay.isControlAll() && !allOwnedByPlayer)
+ if (!selection.every(canControlEntity))
return undefined;
var target = undefined;
if (!fromMinimap)
{
var ent = Engine.PickEntityAtPoint(x, y);
if (ent != INVALID_ENTITY)
target = ent;
}
// decide between the following ordered actions
// if two actions are possible, the first one is taken
// so the most specific should appear first
var actions = Object.keys(g_UnitActions).slice();
actions.sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness);
var actionInfo = undefined;
if (preSelectedAction != ACTION_NONE)
{
for (var action of actions)
if (g_UnitActions[action].preSelectedActionCheck)
{
var r = g_UnitActions[action].preSelectedActionCheck(target, selection);
if (r)
return r;
}
return { "type": "none", "cursor": "", "target": target };
}
for (var action of actions)
if (g_UnitActions[action].hotkeyActionCheck)
{
var r = g_UnitActions[action].hotkeyActionCheck(target, selection);
if (r)
return r;
}
for (var action of actions)
if (g_UnitActions[action].actionCheck)
{
var r = g_UnitActions[action].actionCheck(target, selection);
if (r)
return r;
}
return { "type": "none", "cursor": "", "target": target };
}
+function canControlEntity(ent)
+{
+ let entState = GetEntityState(ent);
+ if (!entState)
+ return false;
+
+ if (entState.player == g_ViewedPlayer)
+ return true;
+
+ let playerState = g_SimState.players[entState.player];
+ return playerState && playerState.controlsAll;
+}
+
function tryPlaceBuilding(queued)
{
if (placementSupport.mode !== "building")
{
error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'");
return false;
}
if (!updateBuildingPlacementPreview())
{
// invalid location - don't build it
// TODO: play a sound?
return false;
}
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "construct",
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
"angle": placementSupport.angle,
"actorSeed": placementSupport.actorSeed,
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
if (!queued)
placementSupport.Reset();
else
placementSupport.RandomizeActorSeed();
return true;
}
function tryPlaceWall(queued)
{
if (placementSupport.mode !== "wall")
{
error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'");
return false;
}
var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...)
if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object"))
{
error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo));
return false;
}
if (!wallPlacementInfo)
return false;
var selection = g_Selection.toList();
var cmd = {
"type": "construct-wall",
"autorepair": true,
"autocontinue": true,
"queued": queued,
"entities": selection,
"wallSet": placementSupport.wallSet,
"pieces": wallPlacementInfo.pieces,
"startSnappedEntity": wallPlacementInfo.startSnappedEnt,
"endSnappedEntity": wallPlacementInfo.endSnappedEnt,
};
// make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end
// point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed
// (this is somewhat non-ideal and hardcode-ish)
var hasWallSegment = false;
for (let piece of cmd.pieces)
{
if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :(
{
hasWallSegment = true;
break;
}
}
if (hasWallSegment)
{
Engine.PostNetworkCommand(cmd);
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
}
return true;
}
/**
* Updates the bandbox object with new positions and visibility.
* @returns {array} The coordinates of the vertices of the bandbox.
*/
function updateBandbox(bandbox, ev, hidden)
{
let scale = +Engine.ConfigDB_GetValue("user", "gui.scale");
let vMin = Vector2D.min(g_DragStart, ev);
let vMax = Vector2D.max(g_DragStart, ev);
bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale);
bandbox.hidden = hidden;
return [vMin.x, vMin.y, vMax.x, vMax.y];
}
// Define some useful unit filters for getPreferredEntities
var unitFilters = {
"isUnit": entity => {
var entState = GetEntityState(entity);
return entState && hasClass(entState, "Unit");
},
"isDefensive": entity => {
var entState = GetEntityState(entity);
return entState && hasClass(entState, "Defensive");
},
"isMilitary": entity => {
var entState = GetEntityState(entity);
return entState &&
g_MilitaryTypes.some(c => hasClass(entState, c));
},
"isNonMilitary": entity => {
var entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
!g_MilitaryTypes.some(c => hasClass(entState, c));
},
"isIdle": entity => {
var entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.unitAI &&
entState.unitAI.isIdle &&
!hasClass(entState, "Domestic");
},
"isWounded": entity => {
let entState = GetEntityState(entity);
return entState &&
hasClass(entState, "Unit") &&
entState.maxHitpoints &&
100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold");
},
"isAnything": entity => {
return true;
}
};
// Choose, inside a list of entities, which ones will be selected.
// We may use several entity filters, until one returns at least one element.
function getPreferredEntities(ents)
{
// Default filters
var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything];
// Handle hotkeys
if (Engine.HotkeyIsPressed("selection.militaryonly"))
filters = [unitFilters.isMilitary];
if (Engine.HotkeyIsPressed("selection.nonmilitaryonly"))
filters = [unitFilters.isNonMilitary];
if (Engine.HotkeyIsPressed("selection.idleonly"))
filters = [unitFilters.isIdle];
if (Engine.HotkeyIsPressed("selection.woundedonly"))
filters = [unitFilters.isWounded];
var preferredEnts = [];
for (var i = 0; i < filters.length; ++i)
{
preferredEnts = ents.filter(filters[i]);
if (preferredEnts.length)
break;
}
return preferredEnts;
}
function handleInputBeforeGui(ev, hoveredObject)
{
if (GetSimState().cinemaPlaying)
return false;
// Capture mouse position so we can use it for displaying cursors,
// and key states
switch (ev.type)
{
case "mousebuttonup":
case "mousebuttondown":
case "mousemotion":
mouseX = ev.x;
mouseY = ev.y;
break;
}
// Remember whether the mouse is over a GUI object or not
mouseIsOverObject = (hoveredObject != null);
// Close the menu when interacting with the game world
if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown")
&& (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT))
g_Menu.close();
// State-machine processing:
//
// (This is for states which should override the normal GUI processing - events will
// be processed here before being passed on, and propagation will stop if this function
// returns true)
//
// TODO: it'd probably be nice to have a better state-machine system, with guaranteed
// entry/exit functions, since this is a bit broken now
switch (inputState)
{
case INPUT_BANDBOXING:
var bandbox = Engine.GetGUIObjectByName("bandbox");
switch (ev.type)
{
case "mousemotion":
var rect = updateBandbox(bandbox, ev, false);
var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
var preferredEntities = getPreferredEntities(ents);
g_Selection.setHighlightList(preferredEntities);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var rect = updateBandbox(bandbox, ev, true);
// Get list of entities limited to preferred entities
var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
// Remove the bandbox hover highlighting
g_Selection.setHighlightList([]);
// Update the list of selected units
if (Engine.HotkeyIsPressed("selection.add"))
{
g_Selection.addList(ents);
}
else if (Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.removeList(ents);
}
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel selection
bandbox.hidden = true;
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_UNIT_POSITION:
switch (ev.type)
{
case "mousemotion":
return positionUnitsFreehandSelectionMouseMove(ev);
case "mousebuttonup":
return positionUnitsFreehandSelectionMouseUp(ev);
}
break;
case INPUT_BUILDING_CLICK:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved far enough from the original click location,
// then switch to drag-orientation mode
let maxDragDelta = 16;
if (g_DragStart.distanceTo(ev) >= maxDragDelta)
{
inputState = INPUT_BUILDING_DRAG;
return false;
}
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
{
inputState = INPUT_BUILDING_PLACEMENT;
}
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_CLICK:
// User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point
// by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode.
switch (ev.type)
{
case "mousebuttonup":
if (ev.button === SDL_BUTTON_LEFT)
{
inputState = INPUT_BUILDING_WALL_PATHING;
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_WALL_PATHING:
// User has chosen a starting point for constructing the wall, and is now looking to set the endpoint.
// Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to
// normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the
// user to continue building walls.
switch (ev.type)
{
case "mousemotion":
placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
// Update the building placement preview, and by extension, the list of snapping candidate entities for both (!)
// the ending point and the starting point to snap to.
//
// TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case
// where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a
// foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on
// the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers
// in them. Might be useful to query only for entities within a certain range around the starting point and ending
// points.
placementSupport.wallSnapEntitiesIncludeOffscreen = true;
var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
if (result && result.cost)
{
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost });
placementSupport.tooltipMessage = [
getEntityCostTooltip(result),
getNeededResourcesTooltip(neededResources)
].filter(tip => tip).join("\n");
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceWall(queued))
{
if (queued)
{
// continue building, just set a new starting position where we left off
placementSupport.position = placementSupport.wallEndPosition;
placementSupport.wallEndPosition = undefined;
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
}
}
else
placementSupport.tooltipMessage = translate("Cannot build wall here!");
updateBuildingPlacementPreview();
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// reset to normal input mode
placementSupport.Reset();
updateBuildingPlacementPreview();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_DRAG:
switch (ev.type)
{
case "mousemotion":
let maxDragDelta = 16;
if (g_DragStart.distanceTo(ev) >= maxDragDelta)
{
// Rotate in the direction of the mouse
placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
}
else
{
// If the mouse is near the center, snap back to the default orientation
placementSupport.SetDefaultAngle();
}
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
updateBuildingPlacementPreview();
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
{
inputState = INPUT_BUILDING_PLACEMENT;
}
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BATCHTRAINING:
if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain")
{
flushTrainingBatch();
inputState = INPUT_NORMAL;
}
break;
}
return false;
}
function handleInputAfterGui(ev)
{
if (GetSimState().cinemaPlaying)
return false;
if (ev.hotkey === undefined)
ev.hotkey = null;
- // Handle the time-warp testing features, restricted to single-player
- if (!g_IsNetworked && g_DeveloperOverlay.isTimeWarpEnabled())
- {
- if (ev.type == "hotkeydown" && ev.hotkey == "session.timewarp.fastforward")
- Engine.SetSimRate(20.0);
- else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward")
- Engine.SetSimRate(1.0);
- else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind")
- Engine.RewindTimeWarp();
- }
-
if (ev.hotkey == "session.highlightguarding")
{
g_ShowGuarding = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
else if (ev.hotkey == "session.highlightguarded")
{
g_ShowGuarded = (ev.type == "hotkeydown");
updateAdditionalHighlight();
}
if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING)
clickedEntity = INVALID_ENTITY;
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_SELECTING;
// If a single click occured, reset the clickedEntity.
// Also set it if we're double/triple clicking and missed the unit earlier.
if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY)
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
if (!controlsPlayer(g_ViewedPlayer))
break;
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_UNIT_POSITION_START;
}
break;
case "hotkeydown":
if (ev.hotkey.indexOf("selection.group.") == 0)
{
let now = Date.now();
if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey)
{
if (ev.hotkey.indexOf("selection.group.select.") == 0)
{
var sptr = ev.hotkey.split(".");
performGroup("snap", sptr[3]);
}
}
else
{
var sptr = ev.hotkey.split(".");
performGroup(sptr[2], sptr[3]);
doublePressTimer = now;
prevHotkey = ev.hotkey;
}
}
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
if (!Engine.HotkeyIsPressed("session.queue"))
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
}
return doAction(action, ev);
}
else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
// else
default:
// Slight hack: If selection is empty, reset the input state
if (g_Selection.toList().length == 0)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved further than a limit, switch to bandbox mode
if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
var ent = Engine.PickEntityAtPoint(ev.x, ev.y);
if (ent != INVALID_ENTITY)
g_Selection.setHighlightList([ent]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
if (clickedEntity == INVALID_ENTITY)
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
// Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event.
if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity))
{
clickedEntity = INVALID_ENTITY;
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.reset();
resetIdleUnit();
}
inputState = INPUT_NORMAL;
return true;
}
// If camera following and we select different unit, stop
if (Engine.GetFollowedEntity() != clickedEntity)
Engine.CameraFollow(0);
var ents = [];
if (ev.clicks == 1)
ents = [clickedEntity];
else
{
// Double click or triple click has occurred
var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
var matchRank = true;
var templateToMatch;
// Check for double click or triple click
if (ev.clicks == 2)
{
// Select similar units regardless of rank
templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName;
if (templateToMatch)
matchRank = false;
else
// No selection group name defined, so fall back to exact match
templateToMatch = GetEntityState(clickedEntity).template;
}
else
// Triple click
// Select units matching exact template name (same rank)
templateToMatch = GetEntityState(clickedEntity).template;
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
}
// Update the list of selected units
if (Engine.HotkeyIsPressed("selection.add"))
g_Selection.addList(ents);
else if (Engine.HotkeyIsPressed("selection.remove"))
g_Selection.removeList(ents);
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_UNIT_POSITION_START:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved further than a limit, switch to unit position mode
if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta))
{
inputState = INPUT_UNIT_POSITION;
return false;
}
break;
case "mousebuttonup":
inputState = INPUT_NORMAL;
if (ev.button == SDL_BUTTON_RIGHT)
{
let action = determineAction(ev.x, ev.y);
if (action)
return doAction(action, ev);
}
break;
}
break;
case INPUT_BUILDING_PLACEMENT:
switch (ev.type)
{
case "mousemotion":
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
if (placementSupport.mode === "wall")
{
// Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is
// still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities
// itself happens in the call to updateBuildingPlacementPreview below).
placementSupport.wallSnapEntitiesIncludeOffscreen = false;
}
else
{
// cancel if not enough resources
if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost }))
{
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementSupport.template,
"x": placementSupport.position.x,
"z": placementSupport.position.z,
});
if (snapData)
{
placementSupport.angle = snapData.angle;
placementSupport.position.x = snapData.x;
placementSupport.position.z = snapData.z;
}
}
updateBuildingPlacementPreview(); // includes an update of the snap entity candidates
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
if (placementSupport.mode === "wall")
{
var validPlacement = updateBuildingPlacementPreview();
if (validPlacement !== false)
inputState = INPUT_BUILDING_WALL_CLICK;
}
else
{
placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y);
g_DragStart = new Vector2D(ev.x, ev.y);
inputState = INPUT_BUILDING_CLICK;
}
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
placementSupport.Reset();
inputState = INPUT_NORMAL;
return true;
}
break;
case "hotkeydown":
var rotation_step = Math.PI / 12; // 24 clicks make a full rotation
switch (ev.hotkey)
{
case "session.rotate.cw":
placementSupport.angle += rotation_step;
updateBuildingPlacementPreview();
break;
case "session.rotate.ccw":
placementSupport.angle -= rotation_step;
updateBuildingPlacementPreview();
break;
}
break;
}
break;
}
return false;
}
function doAction(action, ev)
{
if (!controlsPlayer(g_ViewedPlayer))
return false;
return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action);
}
function positionUnitsFreehandSelectionMouseMove(ev)
{
// Converting the input line into a List of points.
// For better performance the points must have a minimum distance to each other.
let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
if (!g_FreehandSelection_InputLine.length ||
target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >=
g_FreehandSelection_ResolutionInputLineSquared)
g_FreehandSelection_InputLine.push(target);
return false;
}
function positionUnitsFreehandSelectionMouseUp(ev)
{
inputState = INPUT_NORMAL;
let inputLine = g_FreehandSelection_InputLine;
g_FreehandSelection_InputLine = [];
if (ev.button != SDL_BUTTON_RIGHT)
return true;
let lengthOfLine = 0;
for (let i = 1; i < inputLine.length; ++i)
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
let selection = g_Selection.toList().filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
// Checking the line for a minimum length to save performance.
if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits)
{
let action = determineAction(ev.x, ev.y);
return !!action && doAction(action, ev);
}
// Even distribution of the units on the line.
let p0 = inputLine[0];
let entityDistribution = [p0];
let distanceBetweenEnts = lengthOfLine / (selection.length - 1);
let freeDist = -distanceBetweenEnts;
for (let i = 1; i < inputLine.length; ++i)
{
let p1 = inputLine[i];
freeDist += inputLine[i - 1].distanceTo(p1);
while (freeDist >= 0)
{
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1);
entityDistribution.push(p0);
freeDist -= distanceBetweenEnts;
}
}
// Rounding errors can lead to missing or too many points.
entityDistribution = entityDistribution.slice(0, selection.length);
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
entityDistribution.reverse();
Engine.PostNetworkCommand({
"type": Engine.HotkeyIsPressed("session.attackmove") ? "attack-walk-custom" : "walk-custom",
"entities": selection,
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
"queued": Engine.HotkeyIsPressed("session.queue")
});
// Add target markers with a minimum distance of 5 to each other.
let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts);
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker)
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y });
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": selection[0]
});
return true;
}
function handleUnitAction(target, action)
{
if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute)
{
error("Invalid action.type " + action.type);
return false;
}
let selection = g_Selection.toList();
if (Engine.HotkeyIsPressed("session.orderone"))
{
// Pick the first unit that can do this order.
let unit = selection.find(entity =>
["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method =>
g_UnitActions[action.type][method] &&
g_UnitActions[action.type][method](action.target || undefined, [entity])
));
if (unit)
{
selection = [unit];
g_Selection.removeList(selection);
}
}
// If the session.queue hotkey is down, add the order to the unit's order queue instead
// of running it immediately
return g_UnitActions[action.type].execute(target, action, selection, Engine.HotkeyIsPressed("session.queue"));
}
function getEntityLimitAndCount(playerState, entType)
{
let ret = {
"entLimit": undefined,
"entCount": undefined,
"entLimitChangers": undefined,
"canBeAddedCount": undefined
};
if (!playerState.entityLimits)
return ret;
let template = GetTemplateData(entType);
let entCategory = template.trainingRestrictions && template.trainingRestrictions.category ||
template.buildRestrictions && template.buildRestrictions.category;
if (entCategory && playerState.entityLimits[entCategory] !== undefined)
{
ret.entLimit = playerState.entityLimits[entCategory] || 0;
ret.entCount = playerState.entityCounts[entCategory] || 0;
ret.entLimitChangers = playerState.entityLimitChangers[entCategory];
ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0);
}
return ret;
}
// Called by GUI when user clicks construction button
// @param buildTemplate Template name of the entity the user wants to build
function startBuildingPlacement(buildTemplate, playerState)
{
if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0)
return;
// TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI
// to start building a structure, then the highlight selection rings are kept during the construction of the building.
// Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing.
placementSupport.Reset();
// find out if we're building a wall, and change the entity appropriately if so
var templateData = GetTemplateData(buildTemplate);
if (templateData.wallSet)
{
placementSupport.mode = "wall";
placementSupport.wallSet = templateData.wallSet;
inputState = INPUT_BUILDING_PLACEMENT;
}
else
{
placementSupport.mode = "building";
placementSupport.template = buildTemplate;
inputState = INPUT_BUILDING_PLACEMENT;
}
if (templateData.attack &&
templateData.attack.Ranged &&
templateData.attack.Ranged.maxRange)
{
// add attack information to display a good tooltip
placementSupport.attack = templateData.attack;
}
}
// Batch training:
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
// When the user releases shift, or clicks on a different training button, we create the batched units
var g_BatchTrainingEntities;
var g_BatchTrainingType;
var g_NumberOfBatches;
var g_BatchTrainingEntityAllowedCount;
var g_BatchSize = getDefaultBatchTrainingSize();
function OnTrainMouseWheel(dir)
{
if (!Engine.HotkeyIsPressed("session.batchtrain"))
return;
g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio");
if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize))
g_BatchSize = 1;
updateSelectionDetails();
}
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(entity => {
let state = GetEntityState(entity);
return state && state.production && state.production.entities.length &&
state.production.entities.indexOf(trainEntType) != -1;
});
}
function getDefaultBatchTrainingSize()
{
let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize");
return Number.isInteger(num) && num > 0 ? num : 5;
}
function getBatchTrainingSize()
{
return Math.max(Math.round(g_BatchSize), 1);
}
function updateDefaultBatchSize()
{
g_BatchSize = getDefaultBatchTrainingSize();
}
// Add the unit shown at position to the training queue for all entities in the selection
function addTrainingByPosition(position)
{
let playerState = GetSimState().players[Engine.GetPlayerID()];
let selection = g_Selection.toList();
if (!playerState || !selection.length)
return;
let trainableEnts = getAllTrainableEntitiesFromSelection();
let entToTrain = trainableEnts[position];
// When we have no building to train or the position is invalid
if (!entToTrain)
return;
addTrainingToQueue(selection, entToTrain, playerState);
return;
}
// Called by GUI when user clicks training button
function addTrainingToQueue(selection, trainEntType, playerState)
{
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount;
let decrement = Engine.HotkeyIsPressed("selection.remove");
let template;
if (!decrement)
template = GetTemplateData(trainEntType);
// Batch training only possible if we can train at least 2 units
if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1))
{
if (inputState == INPUT_BATCHTRAINING)
{
// Check if we are training in the same building(s) as the last batch
// NOTE: We just check if the arrays are the same and if the order is the same
// If the order changed, we have a new selection and we should create a new batch.
// If we're already creating a batch of this unit (in the same building(s)), then just extend it
// (if training limits allow)
if (g_BatchTrainingEntities.length == selection.length &&
g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) &&
g_BatchTrainingType == trainEntType)
{
if (decrement)
{
--g_NumberOfBatches;
if (g_NumberOfBatches <= 0)
inputState = INPUT_NORMAL;
}
else if (canBeAddedCount == undefined ||
canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length)
{
if (Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize())
}))
return;
++g_NumberOfBatches;
}
g_BatchTrainingEntityAllowedCount = canBeAddedCount;
return;
}
// Otherwise start a new one
else if (!decrement)
flushTrainingBatch();
// fall through to create the new batch
}
// Don't start a new batch if decrementing or unable to afford it.
if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost":
multiplyEntityCosts(template, getBatchTrainingSize()) }))
return;
inputState = INPUT_BATCHTRAINING;
g_BatchTrainingEntities = selection;
g_BatchTrainingType = trainEntType;
g_BatchTrainingEntityAllowedCount = canBeAddedCount;
g_NumberOfBatches = 1;
}
else
{
// Non-batched - just create a single entity in each building
// (but no more than entity limit allows)
let buildingsForTraining = appropriateBuildings;
if (canBeAddedCount !== undefined)
buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount);
Engine.PostNetworkCommand({
"type": "train",
"template": trainEntType,
"count": 1,
"entities": buildingsForTraining
});
}
}
/**
* Returns the number of units that will be present in a batch if the user clicks
* the training button depending on the batch training modifier hotkey
*/
function getTrainingStatus(selection, trainEntType, playerState)
{
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType);
let nextBatchTrainingCount = 0;
let canBeAddedCount;
if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType)
{
nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize();
canBeAddedCount = g_BatchTrainingEntityAllowedCount;
}
else
canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount;
// We need to calculate count after the next increment if it's possible
if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) &&
Engine.HotkeyIsPressed("session.batchtrain"))
nextBatchTrainingCount += getBatchTrainingSize();
nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1);
// If training limits don't allow us to train batchTrainingCount in each appropriate building
// train as many full batches as we can and remainer in one more building.
let buildingsCountToTrainFullBatch = appropriateBuildings.length;
let remainderToTrain = 0;
if (canBeAddedCount !== undefined &&
canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length)
{
buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount);
remainderToTrain = canBeAddedCount % nextBatchTrainingCount;
}
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain];
}
function flushTrainingBatch()
{
let batchedSize = g_NumberOfBatches * getBatchTrainingSize();
let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType);
// If training limits don't allow us to train batchedSize in each appropriate building
if (g_BatchTrainingEntityAllowedCount !== undefined &&
g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length)
{
// Train as many full batches as we can
let buildingsCountToTrainFullBatch = Math.floor( g_BatchTrainingEntityAllowedCount / batchedSize);
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch),
"template": g_BatchTrainingType,
"count": batchedSize
});
// Train remainer in one more building
Engine.PostNetworkCommand({
"type": "train",
"entities": [appropriateBuildings[buildingsCountToTrainFullBatch]],
"template": g_BatchTrainingType,
"count": g_BatchTrainingEntityAllowedCount % batchedSize
});
}
else
Engine.PostNetworkCommand({
"type": "train",
"entities": appropriateBuildings,
"template": g_BatchTrainingType,
"count": batchedSize
});
}
function performGroup(action, groupId)
{
switch (action)
{
case "snap":
case "select":
case "add":
var toSelect = [];
g_Groups.update();
for (var ent in g_Groups.groups[groupId].ents)
toSelect.push(+ent);
if (action != "add")
g_Selection.reset();
g_Selection.addList(toSelect);
if (action == "snap" && toSelect.length)
{
let entState = GetEntityState(toSelect[0]);
let position = entState.position;
if (position && entState.visibility != "hidden")
Engine.CameraMoveTo(position.x, position.z);
}
break;
case "save":
case "breakUp":
g_Groups.groups[groupId].reset();
if (action == "save")
g_Groups.addEntities(groupId, g_Selection.toList());
updateGroups();
break;
}
}
var lastIdleUnit = 0;
var currIdleClassIndex = 0;
var lastIdleClasses = [];
function resetIdleUnit()
{
lastIdleUnit = 0;
currIdleClassIndex = 0;
lastIdleClasses = [];
}
function findIdleUnit(classes)
{
var append = Engine.HotkeyIsPressed("selection.add");
var selectall = Engine.HotkeyIsPressed("selection.offscreen");
// Reset the last idle unit, etc., if the selection type has changed.
if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i]))
resetIdleUnit();
lastIdleClasses = classes;
var data = {
"viewedPlayer": g_ViewedPlayer,
"excludeUnits": append ? g_Selection.toList() : [],
// If the current idle class index is not 0, put the class at that index first.
"idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex))
};
if (!selectall)
{
data.limit = 1;
data.prevUnit = lastIdleUnit;
}
var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data);
if (!idleUnits.length)
{
// TODO: display a message or play a sound to indicate no more idle units, or something
// Reset for next cycle
resetIdleUnit();
return;
}
if (!append)
g_Selection.reset();
g_Selection.addList(idleUnits);
if (selectall)
return;
lastIdleUnit = idleUnits[0];
var entityState = GetEntityState(lastIdleUnit);
var position = entityState.position;
if (position)
Engine.CameraMoveTo(position.x, position.z);
// Move the idle class index to the first class an idle unit was found for.
var indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem));
currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length;
}
function clearSelection()
{
if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
{
inputState = INPUT_NORMAL;
placementSupport.Reset();
}
else
g_Selection.reset();
preSelectedAction = ACTION_NONE;
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 23080)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 23081)
@@ -1,507 +1,512 @@
// Limits selection size
var g_MaxSelectionSize = 200;
// Alpha value of hovered/mouseover/highlighted selection overlays
// (should probably be greater than always visible alpha value,
// see CCmpSelectable)
var g_HighlightedAlpha = 0.75;
function _setHighlight(ents, alpha, selected)
{
if (ents.length)
Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities": ents, "alpha": alpha, "selected": selected });
}
function _setStatusBars(ents, enabled)
{
if (!ents.length)
return;
Engine.GuiInterfaceCall("SetStatusBars", {
"entities": ents,
"enabled": enabled,
"showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true",
"showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true"
});
}
function _setMotionOverlay(ents, enabled)
{
if (ents.length)
Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities": ents, "enabled": enabled });
}
function _playSound(ent)
{
Engine.GuiInterfaceCall("PlaySound", { "name": "select", "entity": ent });
}
/**
* EntityGroups class for managing grouped entities
*/
function EntityGroups()
{
this.groups = {};
this.ents = {};
}
EntityGroups.prototype.reset = function()
{
this.groups = {};
this.ents = {};
};
EntityGroups.prototype.add = function(ents)
{
for (let ent of ents)
{
if (this.ents[ent])
continue;
var entState = GetEntityState(ent);
// When this function is called during group rebuild, deleted
// entities will not yet have been removed, so entities might
// still be present in the group despite not existing.
if (!entState)
continue;
var templateName = entState.template;
var key = GetTemplateData(templateName).selectionGroupName || templateName;
// Group the ents by player and template
if (entState.player !== undefined)
key = "p" + entState.player + "&" + key;
if (this.groups[key])
this.groups[key] += 1;
else
this.groups[key] = 1;
this.ents[ent] = key;
}
};
EntityGroups.prototype.removeEnt = function(ent)
{
var key = this.ents[ent];
// Remove the entity
delete this.ents[ent];
--this.groups[key];
// Remove the entire group
if (this.groups[key] == 0)
delete this.groups[key];
};
EntityGroups.prototype.rebuildGroup = function(renamed)
{
var oldGroup = this.ents;
this.reset();
var toAdd = [];
for (var ent in oldGroup)
toAdd.push(renamed[ent] ? renamed[ent] : +ent);
this.add(toAdd);
};
EntityGroups.prototype.getCount = function(key)
{
return this.groups[key];
};
EntityGroups.prototype.getTotalCount = function()
{
let totalCount = 0;
for (let key in this.groups)
totalCount += this.groups[key];
return totalCount;
};
EntityGroups.prototype.getKeys = function()
{
// Preserve order even when shuffling units around
// Can be optimized by moving the sorting elsewhere
return Object.keys(this.groups).sort();
};
EntityGroups.prototype.getEntsByKey = function(key)
{
var ents = [];
for (var ent in this.ents)
if (this.ents[ent] == key)
ents.push(+ent);
return ents;
};
/**
* get a list of entities grouped by a key
*/
EntityGroups.prototype.getEntsGrouped = function()
{
return this.getKeys().map(key => ({
"ents": this.getEntsByKey(key),
"key": key
}));
};
/**
* Gets all ents in every group except ones of the specified group
*/
EntityGroups.prototype.getEntsByKeyInverse = function(key)
{
var ents = [];
for (var ent in this.ents)
if (this.ents[ent] != key)
ents.push(+ent);
return ents;
};
/**
* EntitySelection class for managing the entity selection list and the primary selection
*/
function EntitySelection()
{
// Private properties:
this.selected = {}; // { id:id, id:id, ... } for each selected entity ID 'id'
// { id:id, ... } for mouseover-highlighted entity IDs in these, the key is a string and the value is an int;
// we want to use the int form wherever possible since it's more efficient to send to the simulation code)
this.highlighted = {};
this.motionDebugOverlay = false;
// Public properties:
this.dirty = false; // set whenever the selection has changed
this.groups = new EntityGroups();
}
/**
* Deselect everything but entities of the chosen type if inverse is true otherwise deselect just the chosen entity
*/
EntitySelection.prototype.makePrimarySelection = function(key, inverse)
{
let ents = inverse ?
this.groups.getEntsByKeyInverse(key) :
this.groups.getEntsByKey(key);
this.reset();
this.addList(ents);
};
/**
* Get a list of the template names
*/
EntitySelection.prototype.getTemplateNames = function()
{
var templateNames = [];
for (let ent in this.selected)
{
let entState = GetEntityState(+ent);
if (entState)
templateNames.push(entState.template);
}
return templateNames;
};
/**
* Update the selection to take care of changes (like units that have been killed)
*/
EntitySelection.prototype.update = function()
{
this.checkRenamedEntities();
+ let controlsAll = g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll;
+ let removeOwnerChanges = !g_IsObserver && !controlsAll && this.toList().length > 1;
+
let changed = false;
- let removeOwnerChanges = !g_IsObserver && !g_DeveloperOverlay.isControlAll() && this.toList().length > 1;
for (let ent in this.selected)
{
let entState = GetEntityState(+ent);
// Remove deleted units
if (!entState)
{
delete this.selected[ent];
this.groups.removeEnt(+ent);
changed = true;
continue;
}
// Remove non-visible units (e.g. moved back into fog-of-war)
// At the next update, mirages will be renamed to the real
// entity they replace, so just ignore them now
// Futhermore, when multiple selection, remove units which have changed ownership
if (entState.visibility == "hidden" && !entState.mirage ||
removeOwnerChanges && entState.player != g_ViewedPlayer)
{
// Disable any highlighting of the disappeared unit
_setHighlight([+ent], 0, false);
_setStatusBars([+ent], false);
_setMotionOverlay([+ent], false);
delete this.selected[ent];
this.groups.removeEnt(+ent);
changed = true;
continue;
}
}
if (changed)
this.onChange();
};
/**
* Update selection if some selected entities were renamed
* (in case of unit promotion or finishing building structure)
*/
EntitySelection.prototype.checkRenamedEntities = function()
{
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
if (renamedEntities.length > 0)
{
var renamedLookup = {};
for (let renamedEntity of renamedEntities)
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
// Reconstruct the selection if at least one entity has been renamed.
for (let renamedEntity of renamedEntities)
if (this.selected[renamedEntity.entity])
{
this.rebuildSelection(renamedLookup);
return;
}
}
};
/**
* Add entities to selection. Play selection sound unless quiet is true
*/
EntitySelection.prototype.addList = function(ents, quiet, force = false)
{
let selection = this.toList();
// If someone else's player is the sole selected unit, don't allow adding to the selection
let firstEntState = selection.length == 1 && GetEntityState(selection[0]);
if (firstEntState && firstEntState.player != g_ViewedPlayer && !force)
return;
let i = 1;
let added = [];
for (let ent of ents)
{
if (selection.length + i > g_MaxSelectionSize)
break;
if (this.selected[ent])
continue;
var entState = GetEntityState(ent);
if (!entState)
continue;
let isUnowned = g_ViewedPlayer != -1 && entState.player != g_ViewedPlayer ||
g_ViewedPlayer == -1 && entState.player == 0;
// Don't add unowned entities to the list, unless a single entity was selected
if (isUnowned && (ents.length > 1 || selection.length) && !force)
continue;
added.push(ent);
this.selected[ent] = ent;
++i;
}
_setHighlight(added, 1, true);
_setStatusBars(added, true);
_setMotionOverlay(added, this.motionDebugOverlay);
if (added.length)
{
// Play the sound if the entity is controllable by us or Gaia-owned.
var owner = GetEntityState(added[0]).player;
if (!quiet && (controlsPlayer(owner) || g_IsObserver || owner == 0))
_playSound(added[0]);
}
this.groups.add(this.toList()); // Create Selection Groups
this.onChange();
};
EntitySelection.prototype.removeList = function(ents)
{
var removed = [];
for (let ent of ents)
if (this.selected[ent])
{
this.groups.removeEnt(ent);
removed.push(ent);
delete this.selected[ent];
}
_setHighlight(removed, 0, false);
_setStatusBars(removed, false);
_setMotionOverlay(removed, false);
this.onChange();
};
EntitySelection.prototype.reset = function()
{
_setHighlight(this.toList(), 0, false);
_setStatusBars(this.toList(), false);
_setMotionOverlay(this.toList(), false);
this.selected = {};
this.groups.reset();
this.onChange();
};
EntitySelection.prototype.rebuildSelection = function(renamed)
{
var oldSelection = this.selected;
this.reset();
var toAdd = [];
for (let ent in oldSelection)
toAdd.push(renamed[ent] || +ent);
this.addList(toAdd, true); // don't play selection sounds
};
EntitySelection.prototype.getFirstSelected = function()
{
for (let ent in this.selected)
return +ent;
return undefined;
};
+/**
+ * TODO: This array should not be recreated every call
+ */
EntitySelection.prototype.toList = function()
{
let ents = [];
for (let ent in this.selected)
ents.push(+ent);
return ents;
};
EntitySelection.prototype.setHighlightList = function(ents)
{
var highlighted = {};
for (let ent of ents)
highlighted[ent] = ent;
var removed = [];
var added = [];
// Remove highlighting for the old units that are no longer highlighted
// (excluding ones that are actively selected too)
for (let ent in this.highlighted)
if (!highlighted[ent] && !this.selected[ent])
removed.push(+ent);
// Add new highlighting for units that aren't already highlighted
for (let ent of ents)
if (!this.highlighted[ent] && !this.selected[ent])
added.push(+ent);
_setHighlight(removed, 0, false);
_setStatusBars(removed, false);
_setHighlight(added, g_HighlightedAlpha, true);
_setStatusBars(added, true);
// Store the new highlight list
this.highlighted = highlighted;
};
EntitySelection.prototype.SetMotionDebugOverlay = function(enabled)
{
this.motionDebugOverlay = enabled;
_setMotionOverlay(this.toList(), enabled);
};
EntitySelection.prototype.onChange = function()
{
this.dirty = true;
if (this.isSelection)
onSelectionChange();
};
/**
* Cache some quantities which depends only on selection
*/
var g_Selection = new EntitySelection();
g_Selection.isSelection = true;
var g_canMoveIntoFormation = {};
var g_allBuildableEntities;
var g_allTrainableEntities;
// Reset cached quantities
function onSelectionChange()
{
g_canMoveIntoFormation = {};
g_allBuildableEntities = undefined;
g_allTrainableEntities = undefined;
}
/**
* EntityGroupsContainer class for managing grouped entities
*/
function EntityGroupsContainer()
{
this.groups = [];
for (var i = 0; i < 10; ++i)
this.groups[i] = new EntityGroups();
}
EntityGroupsContainer.prototype.addEntities = function(groupName, ents)
{
for (let ent of ents)
for (let group of this.groups)
if (ent in group.ents)
group.removeEnt(ent);
this.groups[groupName].add(ents);
};
EntityGroupsContainer.prototype.update = function()
{
this.checkRenamedEntities();
for (let group of this.groups)
for (var ent in group.ents)
{
var entState = GetEntityState(+ent);
// Remove deleted units
if (!entState)
group.removeEnt(ent);
}
};
/**
* Update control group if some entities in the group were renamed
* (in case of unit promotion or finishing building structure)
*/
EntityGroupsContainer.prototype.checkRenamedEntities = function()
{
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
if (renamedEntities.length > 0)
{
var renamedLookup = {};
for (let renamedEntity of renamedEntities)
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
for (let group of this.groups)
for (let renamedEntity of renamedEntities)
// Reconstruct the group if at least one entity has been renamed.
if (renamedEntity.entity in group.ents)
{
group.rebuildGroup(renamedLookup);
break;
}
}
};
var g_Groups = new EntityGroupsContainer();
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23080)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23081)
@@ -1,1410 +1,1419 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_CivData = loadCivData(false, true);
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
var g_Chat;
+var g_DeveloperOverlay;
var g_DiplomacyColors;
var g_DiplomacyDialog;
var g_GameSpeedControl;
var g_Menu;
var g_MiniMapPanel;
var g_ObjectivesDialog;
var g_PauseControl;
var g_PauseOverlay;
var g_PlayerViewControl;
var g_TradeDialog;
var g_TopPanel;
/**
* A random file will be played. TODO: more variety
*/
var g_Ambient = ["audio/ambient/dayscape/day_temperate_gen_03.ogg"];
/**
* Map, player and match settings set in gamesetup.
*/
const g_GameAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes"));
/**
* True if this is a multiplayer game.
*/
const g_IsNetworked = Engine.HasNetClient();
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
var g_IsController = !g_IsNetworked || Engine.HasNetServer();
/**
* Whether we have finished the synchronization and
* can start showing simulation related message boxes.
*/
var g_IsNetworkedActive = false;
/**
* True if the connection to the server has been lost.
*/
var g_Disconnected = false;
/**
* True if the current user has observer capabilities.
*/
var g_IsObserver = false;
/**
* True if the current user has rejoined (or joined the game after it started).
*/
var g_HasRejoined = false;
/**
* Shows a message box asking the user to leave if "won" or "defeated".
*/
var g_ConfirmExit = false;
/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
* True if the camera should focus on attacks and player commands
* and select the affected units.
*/
var g_FollowPlayer = false;
/**
* Cache the basic player data (name, civ, color).
*/
var g_Players = [];
/**
* Last time when onTick was called().
* Used for animating the main menu.
*/
var g_LastTickTime = Date.now();
/**
* Recalculate which units have their status bars shown with this frequency in milliseconds.
*/
var g_StatusBarUpdate = 200;
/**
* For restoring selection, order and filters when returning to the replay menu
*/
var g_ReplaySelectionData;
/**
* Remembers which clients are assigned to which player slots.
* The keys are guids or "local" in Singleplayer.
*/
var g_PlayerAssignments;
-var g_DeveloperOverlay;
-
/**
* Whether the entire UI should be hidden (useful for promotional screenshots).
* Can be toggled with a hotkey.
*/
var g_ShowGUI = true;
/**
* Whether status bars should be shown for all of the player's units.
*/
var g_ShowAllStatusBars = false;
/**
* Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TechnologyData = {};
var g_ResourceData = new Resources();
/**
* These handlers are called each time a new turn was simulated.
* Use this as sparely as possible.
*/
-var g_SimulationUpdateHandlers = [];
+var g_SimulationUpdateHandlers = new Set();
/**
* These handlers are called after the player states have been initialized.
*/
-var g_PlayersInitHandlers = [];
+var g_PlayersInitHandlers = new Set();
/**
* These handlers are called when a player has been defeated or won the game.
*/
-var g_PlayerFinishedHandlers = [];
+var g_PlayerFinishedHandlers = new Set();
/**
* These events are fired whenever the player added or removed entities from the selection.
*/
-var g_EntitySelectionChangeHandlers = [];
+var g_EntitySelectionChangeHandlers = new Set();
/**
* These events are fired when the user has performed a hotkey assignment change.
* Currently only fired on init, but to be fired from any hotkey editor dialog.
*/
var g_HotkeyChangeHandlers = [];
/**
* Top coordinate of the research list.
* Changes depending on the number of displayed counters.
*/
var g_ResearchListTop = 4;
/**
* List of additional entities to highlight.
*/
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
/**
* Display data of the current players entities shown in the top panel.
*/
var g_PanelEntities = [];
/**
* Order in which the panel entities are shown.
*/
var g_PanelEntityOrder = ["Hero", "Relic"];
/**
* Unit classes to be checked for the idle-worker-hotkey.
*/
var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"];
/**
* Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey.
*/
var g_MilitaryTypes = ["Melee", "Ranged"];
function GetSimState()
{
if (!g_SimState)
g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState"));
return g_SimState;
}
function GetMultipleEntityStates(ents)
{
if (!ents.length)
return null;
let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents);
for (let item of entityStates)
g_EntityStates[item.entId] = item.state && deepfreeze(item.state);
return entityStates;
}
function GetEntityState(entId)
{
if (!g_EntityStates[entId])
{
let entityState = Engine.GuiInterfaceCall("GetEntityState", entId);
g_EntityStates[entId] = entityState && deepfreeze(entityState);
}
return g_EntityStates[entId];
}
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = deepfreeze(template);
}
return g_TemplateData[templateName];
}
function GetTechnologyData(technologyName, civ)
{
if (!g_TechnologyData[civ])
g_TechnologyData[civ] = {};
if (!(technologyName in g_TechnologyData[civ]))
{
let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData);
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[civ][technologyName] = deepfreeze(template);
}
return g_TechnologyData[civ][technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
// Fallback used by atlas
g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } };
// Fallback used by atlas and autostart games
if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name)
g_PlayerAssignments.local.name = singleplayerName();
if (initData)
{
g_ReplaySelectionData = initData.replaySelectionData;
g_HasRejoined = initData.isRejoining;
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
}
g_DiplomacyColors = new DiplomacyColors();
g_PlayerViewControl = new PlayerViewControl();
g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors));
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl));
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects);
g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay);
g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates);
g_Chat = new Chat(g_PlayerViewControl);
- g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl);
+ g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection);
g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors);
g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl);
g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes);
g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl);
g_PauseControl = new PauseControl();
g_PauseOverlay = new PauseOverlay(g_PauseControl);
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat);
g_TradeDialog = new TradeDialog(g_PlayerViewControl);
g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl);
initSelectionPanels();
LoadModificationTemplates();
updatePlayerData();
initializeMusic(); // before changing the perspective
initPanelEntities();
Engine.SetBoundingBoxDebugOverlay(false);
updateEnabledRangeOverlayTypes();
for (let handler of g_PlayersInitHandlers)
handler();
for (let handler of g_HotkeyChangeHandlers)
handler();
if (hotloadData)
{
g_Selection.selected = hotloadData.selection;
g_PlayerAssignments = hotloadData.playerAssignments;
g_Players = hotloadData.player;
}
sendLobbyPlayerlistUpdate();
// TODO: use event instead
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
}
function registerPlayersInitHandler(handler)
{
- g_PlayersInitHandlers.push(handler);
+ g_PlayersInitHandlers.add(handler);
}
function registerPlayersFinishedHandler(handler)
{
- g_PlayerFinishedHandlers.push(handler);
+ g_PlayerFinishedHandlers.add(handler);
}
function registerSimulationUpdateHandler(handler)
{
- g_SimulationUpdateHandlers.push(handler);
+ g_SimulationUpdateHandlers.add(handler);
+}
+
+function unregisterSimulationUpdateHandler(handler)
+{
+ g_SimulationUpdateHandlers.delete(handler);
}
function registerEntitySelectionChangeHandler(handler)
{
- g_EntitySelectionChangeHandlers.push(handler);
+ g_EntitySelectionChangeHandlers.add(handler);
+}
+
+function unregisterEntitySelectionChangeHandler(handler)
+{
+ g_EntitySelectionChangeHandlers.delete(handler);
}
function registerHotkeyChangeHandler(handler)
{
g_HotkeyChangeHandlers.push(handler);
}
function updatePlayerData()
{
let simState = GetSimState();
if (!simState)
return;
let playerData = [];
for (let i = 0; i < simState.players.length; ++i)
{
let playerState = simState.players[i];
playerData.push({
"name": playerState.name,
"civ": playerState.civ,
"color": {
"r": playerState.color.r * 255,
"g": playerState.color.g * 255,
"b": playerState.color.b * 255,
"a": playerState.color.a * 255
},
"team": playerState.team,
"teamsLocked": playerState.teamsLocked,
"cheatsEnabled": playerState.cheatsEnabled,
"state": playerState.state,
"isAlly": playerState.isAlly,
"isMutualAlly": playerState.isMutualAlly,
"isNeutral": playerState.isNeutral,
"isEnemy": playerState.isEnemy,
"guid": undefined, // network guid for players controlled by hosts
"offline": g_Players[i] && !!g_Players[i].offline
});
}
for (let guid in g_PlayerAssignments)
{
let playerID = g_PlayerAssignments[guid].player;
if (!playerData[playerID])
continue;
playerData[playerID].guid = guid;
playerData[playerID].name = g_PlayerAssignments[guid].name;
}
g_Players = playerData;
}
/**
* Called when the user changed the diplomacy colors in the options.
* TODO: Remove this proxy and make the options page agnostic of the session page.
*/
function updateDisplayedPlayerColors()
{
g_DiplomacyColors.updateDisplayedPlayerColors();
}
function initPanelEntities()
{
Engine.GetGUIObjectByName("panelEntityPanel").children.forEach((button, slot) => {
button.onPress = function() {
let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot);
if (!panelEnt)
return;
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([panelEnt.ent]);
};
button.onDoublePress = function() {
let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot);
if (panelEnt)
selectAndMoveTo(getEntityOrHolder(panelEnt.ent));
};
});
}
/**
* Returns the entity itself except when garrisoned where it returns its garrisonHolder
*/
function getEntityOrHolder(ent)
{
let entState = GetEntityState(ent);
if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length &&
entState.unitAI.orders[0].type == "Garrison")
return getEntityOrHolder(entState.unitAI.orders[0].data.target);
return ent;
}
function initializeMusic()
{
initMusic();
if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music)
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
global.music.setState(global.music.states.PEACE);
playAmbient();
}
function resetTemplates()
{
// Update GUI and clear player-dependent cache
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
// TODO: do this more selectively
onSimulationUpdate();
}
/**
* Returns true if the player with that ID is in observermode.
*/
function isPlayerObserver(playerID)
{
let playerStates = GetSimState().players;
return !playerStates[playerID] || playerStates[playerID].state != "active";
}
/**
* Returns true if the current user can issue commands for that player.
*/
function controlsPlayer(playerID)
{
let playerStates = GetSimState().players;
return !!playerStates[Engine.GetPlayerID()] &&
playerStates[Engine.GetPlayerID()].controlsAll ||
Engine.GetPlayerID() == playerID &&
!!playerStates[playerID] &&
playerStates[playerID].state != "defeated";
}
/**
* Called when one or more players have won or were defeated.
*
* @param {array} - IDs of the players who have won or were defeated.
* @param {object} - a plural string stating the victory reason.
* @param {boolean} - whether these players have won or lost.
*/
function playersFinished(players, victoryString, won)
{
addChatMessage({
"type": "playerstate",
"message": victoryString,
"players": players
});
if (players.indexOf(Engine.GetPlayerID()) != -1)
reportGame();
sendLobbyPlayerlistUpdate();
updatePlayerData();
// TODO: The other calls in this function should move too
for (let handler of g_PlayerFinishedHandlers)
handler(players, won);
if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning())
return;
global.music.setState(
won ?
global.music.states.VICTORY :
global.music.states.DEFEAT
);
g_ConfirmExit = won ? "won" : "defeated";
}
function resumeGame()
{
g_PauseControl.implicitResume();
}
function closeOpenDialogs()
{
g_Menu.close();
g_Chat.closePage();
g_DiplomacyDialog.close();
g_ObjectivesDialog.close();
g_TradeDialog.close();
}
function endGame()
{
// Before ending the game
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = Engine.GuiInterfaceCall("GetReplayMetadata");
let playerID = Engine.GetPlayerID();
Engine.EndGame();
// After the replay file was closed in EndGame
// Done here to keep EndGame small
if (!g_IsReplay)
Engine.AddReplayToCache(replayDirectory);
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_summary.xml", {
"sim": simData,
"gui": {
"dialog": false,
"assignedPlayer": playerID,
"disconnected": g_Disconnected,
"isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
});
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return {
"selection": g_Selection.selected,
"playerAssignments": g_PlayerAssignments,
"player": g_Players,
};
}
function getSavedGameData()
{
return {
"groups": g_Groups.groups
};
}
function restoreSavedGameData(data)
{
// Restore camera if any
if (data.camera)
Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ,
data.camera.RotX, data.camera.RotY, data.camera.Zoom);
// Clear selection when loading a game
g_Selection.reset();
// Restore control groups
for (let groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
/**
* Called every frame.
*/
function onTick()
{
if (!g_Settings)
return;
let now = Date.now();
let tickLength = now - g_LastTickTime;
g_LastTickTime = now;
handleNetMessages();
updateCursorAndTooltip();
if (g_Selection.dirty)
{
g_Selection.dirty = false;
// When selection changed, get the entityStates of new entities
GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId]));
for (let handler of g_EntitySelectionChangeHandlers)
handler();
updateGUIObjects();
// Display rally points for selected buildings
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength)
recalculateStatusBarDisplay();
updateTimers();
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function onSimulationUpdate()
{
// Templates change depending on technologies and auras, so they have to be reloaded after such a change.
// g_TechnologyData data never changes, so it shouldn't be deleted.
g_EntityStates = {};
if (Engine.GuiInterfaceCall("IsTemplateModified"))
{
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
}
g_SimState = undefined;
if (!GetSimState())
return;
GetMultipleEntityStates(g_Selection.toList());
for (let handler of g_SimulationUpdateHandlers)
handler();
// TODO: Move to handlers
updateCinemaPath();
handleNotifications();
updateGUIObjects();
if (g_ConfirmExit)
confirmExit();
}
/**
* Don't show the message box before all playerstate changes are processed.
*/
function confirmExit()
{
if (g_IsNetworked && !g_IsNetworkedActive)
return;
closeOpenDialogs();
g_PauseControl.implicitPause();
// Don't ask for exit if other humans are still playing
let askExit = !Engine.HasNetServer() || g_Players.every((player, i) =>
i == 0 ||
player.state != "active" ||
g_GameAttributes.settings.PlayerData[i].AI != "");
let subject = g_PlayerStateMessages[g_ConfirmExit];
if (askExit)
subject += "\n" + translate("Do you want to quit?");
messageBox(
400, 200,
subject,
g_ConfirmExit == "won" ?
translate("VICTORIOUS!") :
translate("DEFEATED!"),
askExit ? [translate("No"), translate("Yes")] : [translate("OK")],
askExit ? [resumeGame, endGame] : [resumeGame]);
g_ConfirmExit = false;
}
function toggleGUI()
{
g_ShowGUI = !g_ShowGUI;
updateCinemaPath();
}
function updateCinemaPath()
{
let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected;
Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath;
Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true");
}
// TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead
function updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updatePanelEntities();
displayPanelEntities();
updateGroups();
updateResearchDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
if (!g_IsObserver)
{
// Update music state on basis of battle state.
let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer);
if (battleState)
global.music.setState(global.music.states[battleState]);
}
}
function onReplayFinished()
{
closeOpenDialogs();
g_PauseControl.implicitPause();
messageBox(400, 200,
translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"),
translateWithContext("replayFinished", "Confirmation"),
[translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")],
[resumeGame, endGame]);
}
/**
* updates a status bar on the GUI
* nameOfBar: name of the bar
* points: points to show
* maxPoints: max points
* direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3;
*/
function updateGUIStatusBar(nameOfBar, points, maxPoints, direction)
{
// check, if optional direction parameter is valid.
if (!direction || !(direction >= 0 && direction < 4))
direction = 0;
// get the bar and update it
let statusBar = Engine.GetGUIObjectByName(nameOfBar);
if (!statusBar)
return;
let healthSize = statusBar.size;
let value = 100 * Math.max(0, Math.min(1, points / maxPoints));
// inverse bar
if (direction == 2 || direction == 3)
value = 100 - value;
if (direction == 0)
healthSize.rright = value;
else if (direction == 1)
healthSize.rbottom = value;
else if (direction == 2)
healthSize.rleft = value;
else if (direction == 3)
healthSize.rtop = value;
statusBar.size = healthSize;
}
function updatePanelEntities()
{
let panelEnts =
g_ViewedPlayer == -1 ?
GetSimState().players.reduce((ents, pState) => ents.concat(pState.panelEntities), []) :
GetSimState().players[g_ViewedPlayer].panelEntities;
g_PanelEntities = g_PanelEntities.filter(panelEnt => panelEnts.find(ent => ent == panelEnt.ent));
for (let ent of panelEnts)
{
let panelEntState = GetEntityState(ent);
let template = GetTemplateData(panelEntState.template);
let panelEnt = g_PanelEntities.find(pEnt => ent == pEnt.ent);
if (!panelEnt)
{
panelEnt = {
"ent": ent,
"tooltip": undefined,
"sprite": "stretched:session/portraits/" + template.icon,
"maxHitpoints": undefined,
"currentHitpoints": panelEntState.hitpoints,
"previousHitpoints": undefined
};
g_PanelEntities.push(panelEnt);
}
panelEnt.tooltip = createPanelEntityTooltip(panelEntState, template);
panelEnt.previousHitpoints = panelEnt.currentHitpoints;
panelEnt.currentHitpoints = panelEntState.hitpoints;
panelEnt.maxHitpoints = panelEntState.maxHitpoints;
}
let panelEntIndex = ent => g_PanelEntityOrder.findIndex(entClass =>
GetEntityState(ent).identity.classes.indexOf(entClass) != -1);
g_PanelEntities.sort((panelEntA, panelEntB) =>
panelEntIndex(panelEntA.ent) - panelEntIndex(panelEntB.ent)
).splice(Engine.GetGUIObjectByName("panelEntityPanel").children.length);
}
function createPanelEntityTooltip(panelEntState, template)
{
let getPanelEntNameTooltip = panelEntState => "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]";
return [
getPanelEntNameTooltip,
getCurrentHealthTooltip,
getAttackTooltip,
getArmorTooltip,
getEntityTooltip,
getAurasTooltip
].map(tooltip => tooltip(panelEntState)).filter(tip => tip).join("\n");
}
function displayPanelEntities()
{
let buttons = Engine.GetGUIObjectByName("panelEntityPanel").children;
buttons.forEach((button, slot) => {
if (button.hidden || g_PanelEntities.some(ent => ent.slot !== undefined && ent.slot == slot))
return;
button.hidden = true;
stopColorFade("panelEntityHitOverlay[" + slot + "]");
});
// The slot identifies the button, displayIndex determines its position.
for (let displayIndex = 0; displayIndex < Math.min(g_PanelEntities.length, buttons.length); ++displayIndex)
{
let panelEnt = g_PanelEntities[displayIndex];
// Find the first unused slot if new, otherwise reuse previous.
let slot = panelEnt.slot === undefined ?
buttons.findIndex(button => button.hidden) :
panelEnt.slot;
let panelEntButton = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]");
panelEntButton.tooltip = panelEnt.tooltip;
updateGUIStatusBar("panelEntityHealthBar[" + slot + "]", panelEnt.currentHitpoints, panelEnt.maxHitpoints);
if (panelEnt.slot === undefined)
{
let panelEntImage = Engine.GetGUIObjectByName("panelEntityImage[" + slot + "]");
panelEntImage.sprite = panelEnt.sprite;
panelEntButton.hidden = false;
panelEnt.slot = slot;
}
// If the health of the panelEnt changed since the last update, trigger the animation.
if (panelEnt.previousHitpoints > panelEnt.currentHitpoints)
startColorFade("panelEntityHitOverlay[" + slot + "]", 100, 0,
colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
// TODO: Instead of instant position changes, animate button movement.
setPanelObjectPosition(panelEntButton, displayIndex, buttons.length);
}
}
function updateGroups()
{
g_Groups.update();
// Determine the sum of the costs of a given template
let getCostSum = (ent) => {
let cost = GetTemplateData(GetEntityState(ent).template).cost;
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
};
for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children)
{
Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i;
let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]");
button.hidden = g_Groups.groups[i].getTotalCount() == 0;
button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i);
button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i);
button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i);
// Choose the icon of the most common template (or the most costly if it's not unique)
if (g_Groups.groups[i].getTotalCount() > 0)
{
let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => {
if (pre.ents.length == cur.ents.length)
return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur;
return pre.ents.length > cur.ents.length ? pre : cur;
}).ents[0]).template).icon;
Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite =
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
}
setPanelObjectPosition(button, i, 1);
}
}
function selectAndMoveTo(ent)
{
let entState = GetEntityState(ent);
if (!entState || !entState.position)
return;
g_Selection.reset();
g_Selection.addList([ent]);
let position = entState.position;
Engine.CameraMoveTo(position.x, position.z);
}
function updateResearchDisplay()
{
let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer);
// Set up initial positioning.
let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right;
for (let i = 0; i < 10; ++i)
{
let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
let size = button.size;
size.top = g_ResearchListTop + (4 + buttonSideLength) * i;
size.bottom = size.top + buttonSideLength;
button.size = size;
}
let numButtons = 0;
for (let tech in researchStarted)
{
// Show at most 10 in-progress techs.
if (numButtons >= 10)
break;
let template = GetTechnologyData(tech, g_Players[g_ViewedPlayer].civ);
let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]");
button.hidden = false;
button.tooltip = getEntityNames(template);
button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher);
let icon = "stretched:session/portraits/" + template.icon;
Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;
// Scale the progress indicator.
let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left));
Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size;
Engine.GetGUIObjectByName("researchStartedTimeRemaining[" + numButtons + "]").caption =
Engine.FormatMillisecondsIntoDateStringGMT(researchStarted[tech].timeRemaining, translateWithContext("countdown format", "m:ss"));
++numButtons;
}
// Hide unused buttons.
for (let i = numButtons; i < 10; ++i)
Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}
/**
* Toggles the display of status bars for all of the player's entities.
*
* @param {Boolean} remove - Whether to hide all previously shown status bars.
*/
function recalculateStatusBarDisplay(remove = false)
{
let entities;
if (g_ShowAllStatusBars && !remove)
entities = g_ViewedPlayer == -1 ?
Engine.PickNonGaiaEntitiesOnScreen() :
Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer);
else
{
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
// Remove selected entities from the 'all entities' array,
// to avoid disabling their status bars.
entities = Engine.GuiInterfaceCall(
g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", {
"viewedPlayer": g_ViewedPlayer
}).filter(idx => selected.indexOf(idx) == -1);
}
Engine.GuiInterfaceCall("SetStatusBars", {
"entities": entities,
"enabled": g_ShowAllStatusBars && !remove,
"showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true",
"showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true"
});
}
function removeStatusBarDisplay()
{
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay(true);
}
/**
* Inverts the given configuration boolean and returns the current state.
* For example "silhouettes".
*/
function toggleConfigBool(configName)
{
let enabled = Engine.ConfigDB_GetValue("user", configName) != "true";
Engine.ConfigDB_CreateAndWriteValueToFile("user", configName, String(enabled), "config/user.cfg");
return enabled;
}
/**
* Toggles the display of range overlays of selected entities for the given range type.
* @param {string} type - for example "Auras"
*/
function toggleRangeOverlay(type)
{
let enabled = toggleConfigBool("gui.session." + type.toLowerCase() + "range");
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": type,
"enabled": enabled
});
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
Engine.GuiInterfaceCall("SetRangeOverlays", {
"entities": selected,
"enabled": enabled
});
}
function updateEnabledRangeOverlayTypes()
{
for (let type of ["Attack", "Auras", "Heal"])
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": type,
"enabled": Engine.ConfigDB_GetValue("user", "gui.session." + type.toLowerCase() + "range") == "true"
});
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
let entsAdd = []; // list of entities units to be highlighted
let entsRemove = [];
let highlighted = g_Selection.toList();
for (let ent in g_Selection.highlighted)
highlighted.push(g_Selection.highlighted[ent]);
if (g_ShowGuarding)
// flag the guarding entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.guard || !state.guard.entities.length)
continue;
for (let ent of state.guard.entities)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
if (g_ShowGuarded)
// flag the guarded entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
let ent = state.unitAI.isGuarding;
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
// flag the entities to remove (from the previously added) from this additional highlight
for (let ent of g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd, g_HighlightedAlpha, true);
_setHighlight(entsRemove, 0, false);
g_AdditionalHighlight = entsAdd;
}
function playAmbient()
{
Engine.PlayAmbientSound(pickRandom(g_Ambient), true);
}
/**
* Adds the ingame time and ceasefire counter to the global FPS and
* realtime counters shown in the top right corner.
*/
function appendSessionCounters(counters)
{
let simState = GetSimState();
if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true")
{
let currentSpeed = Engine.GetSimRate();
if (currentSpeed != 1.0)
// Translation: The "x" means "times", with the mathematical meaning of multiplication.
counters.push(sprintf(translate("%(time)s (%(speed)sx)"), {
"time": timeToString(simState.timeElapsed),
"speed": Engine.FormatDecimalNumberIntoString(currentSpeed)
}));
else
counters.push(timeToString(simState.timeElapsed));
}
if (simState.ceasefireActive && Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true")
counters.push(timeToString(simState.ceasefireTimeRemaining));
g_ResearchListTop = 4 + 14 * counters.length;
}
/**
* Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function sendLobbyPlayerlistUpdate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
// Extract the relevant player data and minimize packet load
let minPlayerData = [];
for (let playerID in g_GameAttributes.settings.PlayerData)
{
if (+playerID == 0)
continue;
let pData = g_GameAttributes.settings.PlayerData[playerID];
let minPData = { "Name": pData.Name, "Civ": pData.Civ };
if (g_GameAttributes.settings.LockTeams)
minPData.Team = pData.Team;
if (pData.AI)
{
minPData.AI = pData.AI;
minPData.AIDiff = pData.AIDiff;
minPData.AIBehavior = pData.AIBehavior;
}
if (g_Players[playerID].offline)
minPData.Offline = true;
// Whether the player has won or was defeated
let state = g_Players[playerID].state;
if (state != "active")
minPData.State = state;
minPlayerData.push(minPData);
}
// Add observers
let connectedPlayers = 0;
for (let guid in g_PlayerAssignments)
{
let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player];
if (pData)
++connectedPlayers;
else
minPlayerData.push({
"Name": g_PlayerAssignments[guid].name,
"Team": "observer"
});
}
Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData));
}
/**
* Send a report on the gamestatus to the lobby.
* Keep in sync with source/tools/XpartaMuPP/LobbyRanking.py
*/
function reportGame()
{
// Only 1v1 games are rated (and Gaia is part of g_Players)
if (!Engine.HasXmppClient() || !Engine.IsRankedGame() ||
g_Players.length != 3 || Engine.GetPlayerID() == -1)
return;
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
let unitsClasses = [
"total",
"Infantry",
"Worker",
"FemaleCitizen",
"Cavalry",
"Champion",
"Hero",
"Siege",
"Ship",
"Trader"
];
let unitsCountersTypes = [
"unitsTrained",
"unitsLost",
"enemyUnitsKilled"
];
let buildingsClasses = [
"total",
"CivCentre",
"House",
"Economic",
"Outpost",
"Military",
"Fortress",
"Wonder"
];
let buildingsCountersTypes = [
"buildingsConstructed",
"buildingsLost",
"enemyBuildingsDestroyed"
];
let resourcesTypes = [
"wood",
"food",
"stone",
"metal"
];
let resourcesCounterTypes = [
"resourcesGathered",
"resourcesUsed",
"resourcesSold",
"resourcesBought"
];
let misc = [
"tradeIncome",
"tributesSent",
"tributesReceived",
"treasuresCollected",
"lootCollected",
"percentMapExplored"
];
let playerStatistics = {};
// Unit Stats
for (let unitCounterType of unitsCountersTypes)
{
if (!playerStatistics[unitCounterType])
playerStatistics[unitCounterType] = { };
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] = "";
}
playerStatistics.unitsLostValue = "";
playerStatistics.unitsKilledValue = "";
// Building stats
for (let buildingCounterType of buildingsCountersTypes)
{
if (!playerStatistics[buildingCounterType])
playerStatistics[buildingCounterType] = { };
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] = "";
}
playerStatistics.buildingsLostValue = "";
playerStatistics.enemyBuildingsDestroyedValue = "";
// Resources
for (let resourcesCounterType of resourcesCounterTypes)
{
if (!playerStatistics[resourcesCounterType])
playerStatistics[resourcesCounterType] = { };
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] = "";
}
playerStatistics.resourcesGathered.vegetarianFood = "";
for (let type of misc)
playerStatistics[type] = "";
// Total
playerStatistics.economyScore = "";
playerStatistics.militaryScore = "";
playerStatistics.totalScore = "";
let mapName = g_GameAttributes.settings.Name;
let playerStates = "";
let playerCivs = "";
let teams = "";
let teamsLocked = true;
// Serialize the statistics for each player into a comma-separated list.
// Ignore gaia
for (let i = 1; i < extendedSimState.players.length; ++i)
{
let player = extendedSimState.players[i];
let maxIndex = player.sequences.time.length - 1;
playerStates += player.state + ",";
playerCivs += player.civ + ",";
teams += player.team + ",";
teamsLocked = teamsLocked && player.teamsLocked;
for (let resourcesCounterType of resourcesCounterTypes)
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] += player.sequences[resourcesCounterType][resourcesType][maxIndex] + ",";
playerStatistics.resourcesGathered.vegetarianFood += player.sequences.resourcesGathered.vegetarianFood[maxIndex] + ",";
for (let unitCounterType of unitsCountersTypes)
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] += player.sequences[unitCounterType][unitsClass][maxIndex] + ",";
for (let buildingCounterType of buildingsCountersTypes)
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] += player.sequences[buildingCounterType][buildingsClass][maxIndex] + ",";
let total = 0;
for (let type in player.sequences.resourcesGathered)
total += player.sequences.resourcesGathered[type][maxIndex];
playerStatistics.economyScore += total + ",";
playerStatistics.militaryScore += Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10) + ",";
playerStatistics.totalScore += (total + Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10)) + ",";
for (let type of misc)
playerStatistics[type] += player.sequences[type][maxIndex] + ",";
}
// Send the report with serialized data
let reportObject = {};
reportObject.timeElapsed = extendedSimState.timeElapsed;
reportObject.playerStates = playerStates;
reportObject.playerID = Engine.GetPlayerID();
reportObject.matchID = g_GameAttributes.matchID;
reportObject.civs = playerCivs;
reportObject.teams = teams;
reportObject.teamsLocked = String(teamsLocked);
reportObject.ceasefireActive = String(extendedSimState.ceasefireActive);
reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining);
reportObject.mapName = mapName;
reportObject.economyScore = playerStatistics.economyScore;
reportObject.militaryScore = playerStatistics.militaryScore;
reportObject.totalScore = playerStatistics.totalScore;
for (let rct of resourcesCounterTypes)
for (let rt of resourcesTypes)
reportObject[rt + rct.substr(9)] = playerStatistics[rct][rt];
// eg. rt = food rct.substr = Gathered rct = resourcesGathered
reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood;
for (let type of unitsClasses)
{
// eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsTrained"] = playerStatistics.unitsTrained[type];
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "UnitsLost"] = playerStatistics.unitsLost[type];
reportObject["enemy" + type + "UnitsKilled"] = playerStatistics.enemyUnitsKilled[type];
}
for (let type of buildingsClasses)
{
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsConstructed"] = playerStatistics.buildingsConstructed[type];
reportObject[(type.substr(0, 1)).toLowerCase() + type.substr(1) + "BuildingsLost"] = playerStatistics.buildingsLost[type];
reportObject["enemy" + type + "BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type];
}
for (let type of misc)
reportObject[type] = playerStatistics[type];
Engine.SendGameReport(reportObject);
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 23080)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 23081)
@@ -1,153 +1,145 @@
+
onTick();
restoreSavedGameData(arguments[0]);
onSimulationUpdate();
onReplayFinished();
onReplayOutOfSync(arguments[0], arguments[1], arguments[2]);
Engine.ConfigDB_CreateValue("user", "gui.session.timeelapsedcounter", String(Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") != "true"));
Engine.ConfigDB_CreateValue("user", "gui.session.ceasefirecounter", String(Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") != "true"));
ExitendGame();
-
-
-
+
-
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 23080)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 23081)
@@ -1,1567 +1,1568 @@
/**
* Specifies which template should indicate the target location of a player command,
* given a command type.
*/
var g_TargetMarker = {
"move": "special/target_marker"
};
/**
* Which enemy entity types will be attacked on sight when patroling.
*/
var g_PatrolTargets = ["Unit"];
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var g_UnitActions =
{
"move":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "walk",
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
if (!someUnitAI(selection) ||
!Engine.HotkeyIsPressed("session.move") ||
!getActionInfo("move", target, selection).possible)
return false;
return { "type": "move" };
},
"actionCheck": function(target, selection)
{
if (!someUnitAI(selection) || !getActionInfo("move", target, selection).possible)
return false;
return { "type": "move" };
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued)
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
Engine.PostNetworkCommand({
"type": "attack-walk",
"entities": selection,
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
"queued": queued
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
if (!someUnitAI(selection) ||
!Engine.HotkeyIsPressed("session.attackmove") ||
!getActionInfo("attack-move", target, selection).possible)
return false;
return {
"type": "attack-move",
"cursor": "action-attack-move"
};
},
"specificness": 30,
},
"capture":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"allowCapture": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["Capture"]
})
};
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("capture", target, selection).possible)
return false;
return {
"type": "capture",
"cursor": "action-capture",
"target": target
};
},
"specificness": 9,
},
"attack":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"queued": queued,
"allowCapture": false
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["!Capture"]
})
};
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.attack") ||
!getActionInfo("attack", target, selection).possible)
return false;
return {
"type": "attack",
"cursor": "action-attack",
"target": target
};
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("attack", target, selection).possible)
return false;
return {
"type": "attack",
"cursor": "action-attack",
"target": target
};
},
"specificness": 10,
},
"patrol":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "patrol",
"entities": selection,
"x": target.x,
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
"queued": queued,
"allowCapture": false
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", { "name": "order_patrol", "entity": selection[0] });
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.canPatrol)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
if (!someCanPatrol(selection) ||
!Engine.HotkeyIsPressed("session.patrol") ||
!getActionInfo("patrol", target, selection).possible)
return false;
return {
"type": "patrol",
"cursor": "action-patrol",
"target": target
};
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_PATROL || !getActionInfo("patrol", target, selection).possible)
return false;
return {
"type": "patrol",
"cursor": "action-patrol",
"target": target
};
},
"specificness": 37,
},
"heal":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "heal",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_heal",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.heal ||
!hasClass(targetState, "Unit") || !targetState.needsHeal ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
entState.id == targetState.id) // Healers can't heal themselves.
return false;
let unhealableClasses = entState.heal.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
let healableClasses = entState.heal.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("heal", target, selection).possible)
return false;
return {
"type": "heal",
"cursor": "action-heal",
"target": target
};
},
"specificness": 7,
},
"repair":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "repair",
"entities": selection,
"target": action.target,
"autocontinue": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_repair",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.builder ||
!targetState.needsRepair && !targetState.foundation ||
!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_REPAIR)
return false;
if (getActionInfo("repair", target, selection).possible)
return {
"type": "repair",
"cursor": "action-repair",
"target": target
};
return {
"type": "none",
"cursor": "action-repair-disabled",
"target": null
};
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.repair") ||
!getActionInfo("repair", target, selection).possible)
return false;
return {
"type": "repair",
"cursor": "action-repair",
"target": target
};
},
"actionCheck": function(target, selection)
{
if (!getActionInfo("repair", target, selection).possible)
return false;
return {
"type": "repair",
"cursor": "action-repair",
"target": target
};
},
"specificness": 11,
},
"gather":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "gather",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceSupply)
return false;
let resource = findGatherType(entState, targetState.resourceSupply);
if (!resource)
return false;
return {
"possible": true,
"cursor": "action-gather-" + resource
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("gather", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "gather",
"cursor": actionInfo.cursor,
"target": target
};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "returnresource",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.resourceDropsite)
return false;
let playerState = GetSimState().players[entState.player];
if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
{
if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
}
else if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
let carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {
"possible": true,
"cursor": "action-return-" + carriedType
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("returnresource", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "returnresource",
"cursor": actionInfo.cursor,
"target": target
};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "setup-trade-route",
"entities": selection,
"target": action.target,
"source": null,
"route": null,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_trade",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails)
return false;
let tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.") + "\n";
if (tradingDetails.hasBothMarkets)
tooltip += sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
else
tooltip += translate("Right-click on another market to set it as a destination trade market.");
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0) // markets too close
return false;
tooltip = translate("Right-click to set as destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
}
return {
"possible": true,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("setup-trade-route", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "setup-trade-route",
"cursor": "action-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target
};
},
"specificness": 0,
},
"garrison":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "garrison",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.canGarrison || !targetState.garrisonHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount,
"capacity": targetState.garrisonHolder.capacity
});
let extraCount = 0;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.garrisonedEntitiesCount;
if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return false;
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_GARRISON)
return false;
let actionInfo = getActionInfo("garrison", target, selection);
if (!actionInfo.possible)
return {
"type": "none",
"cursor": "action-garrison-disabled",
"target": null
};
return {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target
};
},
"hotkeyActionCheck": function(target, selection)
{
let actionInfo = getActionInfo("garrison", target, selection);
if (!Engine.HotkeyIsPressed("session.garrison") || !actionInfo.possible)
return false;
return {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target
};
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "guard",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": selection[0]
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState.guard ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
!entState.unitAI || !entState.unitAI.canGuard ||
targetState.unitAI && targetState.unitAI.isGuarding)
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
if (preSelectedAction != ACTION_GUARD)
return false;
if (getActionInfo("guard", target, selection).possible)
return {
"type": "guard",
"cursor": "action-guard",
"target": target
};
return {
"type": "none",
"cursor": "action-guard-disabled",
"target": null
};
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.guard") ||
!getActionInfo("guard", target, selection).possible)
return false;
return {
"type": "guard",
"cursor": "action-guard",
"target": target
};
},
"specificness": 40,
},
"remove-guard":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "remove-guard",
"entities": selection,
"target": action.target,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": selection[0]
});
return true;
},
"hotkeyActionCheck": function(target, selection)
{
if (!Engine.HotkeyIsPressed("session.guard") ||
!getActionInfo("remove-guard", target, selection).possible ||
!someGuarding(selection))
return false;
return {
"type": "remove-guard",
"cursor": "action-remove-guard"
};
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({
"type": "set-rallypoint",
"entities": selection,
"x": target.x,
"z": target.z,
"data": action.data,
"queued": queued
});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
let tooltip;
// default to walking there (or attack-walking if hotkey pressed)
let data = { "command": "walk" };
let cursor = "";
if (Engine.HotkeyIsPressed("session.attackmove"))
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
data.command = "attack-walk";
data.targetClasses = targetClasses;
cursor = "action-attack-move";
}
if (Engine.HotkeyIsPressed("session.repair") &&
(targetState.needsRepair || targetState.foundation) &&
playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState.garrisonHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount,
"capacity": targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.garrisonedEntitiesCount >=
targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
if (resourceType.generic == "treasure")
cursor = "action-gather-" + resourceType.generic;
else
cursor = "action-gather-" + resourceType.specific;
data.command = "gather-near-position";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
if (!targetState.speed)
{
data.command = "gather";
data.target = targetState.id;
}
}
else if (entState.market && targetState.market &&
entState.id != targetState.id &&
(!entState.market.naval || targetState.market.naval) &&
!playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this building can produce.
let trader;
if (entState.production && entState.production.entities.length)
for (let i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
break;
let traderData = {
"firstMarket": entState.id,
"secondMarket": targetState.id,
"template": trader
};
let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain && gain.traderGain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
sprintf(
trader ?
translate("Gain: %(gain)s") :
translate("Expected gain: %(gain)s"),
{ "gain": getTradingTooltip(gain) });
}
}
else if ((targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (playerCheck(entState, targetState, ["Enemy"]))
{
data.target = targetState.id;
data.command = "attack";
cursor = "action-attack";
}
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities
if (!Engine.HotkeyIsPressed("session.autorallypoint") ||
!targetState.production ||
!targetState.production.entities.length)
for (let ent in g_Selection.selected)
if (targetState.id == +ent)
return false;
return {
"possible": true,
"data": data,
"position": targetState.position,
"cursor": cursor,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
if (someUnitAI(selection) || !someRallyPoints(selection))
return false;
let actionInfo = getActionInfo("set-rallypoint", target, selection);
if (!actionInfo.possible)
return false;
return {
"type": "set-rallypoint",
"cursor": actionInfo.cursor,
"data": actionInfo.data,
"tooltip": actionInfo.tooltip,
"position": actionInfo.position
};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "unset-rallypoint",
"entities": selection
});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (entState.id != targetState.id ||
!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
if (someUnitAI(selection) || !someRallyPoints(selection) ||
!getActionInfo("unset-rallypoint", target, selection).possible)
return false;
return {
"type": "unset-rallypoint",
"cursor": "action-unset-rally"
};
},
"specificness": 11,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central panel
*/
var g_EntityCommands =
{
"unload-all": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
if (entState.garrisonHolder)
count += entState.garrisonHolder.entities.length;
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
};
},
"execute": function()
{
unloadAll();
},
},
"delete": {
"getInfo": function(entStates)
{
return entStates.some(entState => !isUndeletable(entState)) ?
{
"tooltip":
colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
translate("Destroy the selected units or buildings.") + "\n" +
colorizeHotkey(
translate("Use %(hotkey)s to avoid the confirmation dialog."),
"session.noconfirmation"
),
"icon": "kill_small.png"
} :
{
// Get all delete reasons and remove duplications
"tooltip": entStates.map(entState => isUndeletable(entState))
.filter((reason, pos, self) =>
self.indexOf(reason) == pos && reason
).join("\n"),
"icon": "kill_small_disabled.png"
};
},
"execute": function(entStates)
{
let entityIDs = entStates.reduce(
(ids, entState) => {
if (!isUndeletable(entState))
ids.push(entState.id);
return ids;
},
[]);
if (!entityIDs.length)
return;
let deleteSelection = () => Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": entityIDs
});
if (Engine.HotkeyIsPressed("session.noconfirmation"))
deleteSelection();
else
{
closeOpenDialogs();
g_PauseControl.implicitPause();
messageBox(
400, 200,
translate("Destroy everything currently selected?"),
translate("Delete"),
[translate("No"), translate("Yes")],
[
resumeGame,
() => {
deleteSelection();
resumeGame();
}
]);
};
},
},
"stop": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
translate("Abort the current order."),
"icon": "stop.png"
};
},
"execute": function(entStates)
{
if (entStates.length)
stopUnits(entStates.map(entState => entState.id));
},
},
"garrison": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || entState.turretParent || false))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
translate("Order the selected units to garrison in a building or unit."),
"icon": "garrison.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
},
"unload": {
"getInfo": function(entStates)
{
if (entStates.every(entState => {
if (!entState.unitAI || !entState.turretParent)
return true;
let parent = GetEntityState(entState.turretParent);
return !parent || !parent.garrisonHolder || parent.garrisonHolder.entities.indexOf(entState.id) == -1;
}))
return false;
return {
"tooltip": translate("Unload"),
"icon": "garrison-out.png"
};
},
"execute": function()
{
unloadSelection();
},
},
"repair": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.builder))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
translate("Order the selected units to repair a building or mechanical unit."),
"icon": "repair.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
},
"focus-rally": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.rallyPoint))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
translate("Focus on Rally Point."),
"icon": "focus-rally.png"
};
},
"execute": function(entStates)
{
// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
let focusTarget;
for (let entState of entStates)
if (entState.rallyPoint && entState.rallyPoint.position)
{
focusTarget = entState.rallyPoint.position;
break;
}
if (!focusTarget)
for (let entState of entStates)
if (entState.position)
{
focusTarget = entState.position;
break;
}
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
},
"back-to-work": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
translate("Back to Work"),
"icon": "production.png"
};
},
"execute": function()
{
backToWork();
},
},
"add-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState =>
!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
translate("Order the selected units to guard a building or unit."),
"icon": "add-guard.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
},
"remove-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png"
};
},
"execute": function()
{
removeGuard();
},
},
"select-trading-goods": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.market))
return false;
return {
"tooltip": translate("Barter & Trade"),
"icon": "economics.png"
};
},
"execute": function()
{
g_TradeDialog.toggle();
},
},
"patrol": {
"getInfo": function(entStates)
{
if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
translate("Patrol") + "\n" +
translate("Attack all encountered enemy units while avoiding buildings."),
"icon": "patrol.png"
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_PATROL;
},
},
"share-dropsite": {
"getInfo": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!sharableEntities.length)
return false;
// Returns if none of the entities belong to a player with a mutual ally
if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
(isAlly, playerId) => isAlly && playerId != entState.player)))
return false;
return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
{
"tooltip": translate("Press to allow allies to use this dropsite"),
"icon": "locked_small.png"
} :
{
"tooltip": translate("Press to prevent allies from using this dropsite"),
"icon": "unlocked_small.png"
};
},
"execute": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
Engine.PostNetworkCommand({
"type": "set-dropsite-sharing",
"entities": sharableEntities.map(entState => entState.id),
"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
});
},
}
};
var g_AllyEntityCommands =
{
"unload-all": {
"getInfo": function(entState)
{
if (!entState.garrisonHolder)
return false;
let player = Engine.GetPlayerID();
let count = 0;
for (let ent in g_Selection.selected)
{
let selectedEntState = GetEntityState(+ent);
if (!selectedEntState.garrisonHolder)
continue;
for (let entity of selectedEntState.garrisonHolder.entities)
{
let state = GetEntityState(entity);
if (state.player == player)
++count;
}
}
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
};
},
"execute": function(entState)
{
unloadAll();
},
},
"share-dropsite": {
"getInfo": function(entState)
{
if (Engine.GetPlayerID() == -1 ||
!GetSimState().players[Engine.GetPlayerID()].hasSharedDropsites ||
!entState.resourceDropsite || !entState.resourceDropsite.sharable)
return false;
if (entState.resourceDropsite.shared)
return {
"tooltip": translate("You are allowed to use this dropsite"),
"icon": "unlocked_small.png"
};
return {
"tooltip": translate("The use of this dropsite is prohibited"),
"icon": "locked_small.png"
};
},
"execute": function(entState)
{
// This command button is always disabled
},
}
};
function playerCheck(entState, targetState, validPlayers)
{
let playerState = GetSimState().players[entState.player];
for (let player of validPlayers)
if (player == "Gaia" && targetState.player == 0 ||
player == "Player" && targetState.player == entState.player ||
playerState["is" + player] && playerState["is" + player][targetState.player])
return true;
return false;
}
function hasClass(entState, className)
{
// note: use the functions in globalscripts/Templates.js for more versatile matching
return entState.identity && entState.identity.classes.indexOf(className) != -1;
}
/**
* Work out whether at least part of the selected entities have UnitAI.
*/
function someUnitAI(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.unitAI || false;
});
}
function someRallyPoints(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.rallyPoint || false;
});
}
function someGuarding(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.unitAI && entState.unitAI.isGuarding;
});
}
function someCanPatrol(entities)
{
return entities.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.unitAI && entState.unitAI.canPatrol;
});
}
/**
* Keep in sync with Commands.js.
*/
function isUndeletable(entState)
{
- if (g_DeveloperOverlay.isControlAll())
+ let playerState = g_SimState.players[entState.player];
+ if (playerState && playerState.controlsAll)
return false;
if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
return translate("The entity has to be killed before it can be gathered from");
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
return translate("You cannot destroy this entity as you own less than half the capture points");
if (!entState.identity.canDelete)
return translate("This entity is undeletable");
return false;
}
function DrawTargetMarker(target)
{
Engine.GuiInterfaceCall("AddTargetMarker", {
"template": g_TargetMarker.move,
"x": target.x,
"z": target.z
});
}
function findGatherType(gatherer, supply)
{
if (!gatherer.resourceGatherRates || !supply)
return undefined;
if (gatherer.resourceGatherRates[supply.type.generic + "." + supply.type.specific])
return supply.type.specific;
if (gatherer.resourceGatherRates[supply.type.generic])
return supply.type.generic;
return undefined;
}
function getActionInfo(action, target, selection)
{
let simState = GetSimState();
// If the selection doesn't exist, no action
if (!GetEntityState(selection[0]))
return { "possible": false };
if (!target) // TODO move these non-target actions to an object like unit_actions.js
{
if (action == "set-rallypoint")
{
let cursor = "";
let data = { "command": "walk" };
if (Engine.HotkeyIsPressed("session.attackmove"))
{
data.command = "attack-walk";
data.targetClasses = Engine.HotkeyIsPressed("session.attackmoveUnit") ?
{ "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] };
cursor = "action-attack-move";
}
else if (Engine.HotkeyIsPressed("session.patrol"))
{
data.command = "patrol";
data.targetClasses = { "attack": g_PatrolTargets };
cursor = "action-patrol";
}
return {
"possible": true,
"data": data,
"cursor": cursor
};
}
return {
"possible": ["move", "attack-move", "remove-guard", "patrol"].indexOf(action) != -1
};
}
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
let targetState = GetEntityState(target);
if (!targetState)
return { "possible": false };
// Check if any entities in the selection can do some of the available actions with target
for (let entityID of selection)
{
let entState = GetEntityState(entityID);
if (!entState)
continue;
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
{
let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
if (r && r.possible) // return true if it's possible for one of the entities
return r;
}
}
return { "possible": false };
}