diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg
index 28ef8ee526..12fd965d2b 100644
--- a/binaries/data/config/default.cfg
+++ b/binaries/data/config/default.cfg
@@ -341,6 +341,8 @@ rotate.cw = RightBracket ; Rotate building placement preview clockwise
rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise
snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure
toggledefaultformation = "" ; Switch between null default formation and the last default formation used (defaults to "box")
+flare = K ; Modifier to send a flare to your allies
+flareactivate = "" ; Modifier to activate the mode to send a flare to your allies
; Overlays
showstatusbars = Tab ; Toggle display of status bars
devcommands.toggle = "Alt+D" ; Toggle developer commands panel
diff --git a/binaries/data/mods/mod/gui/gui.rng b/binaries/data/mods/mod/gui/gui.rng
index 6b02e25926..b5788c88f8 100644
--- a/binaries/data/mods/mod/gui/gui.rng
+++ b/binaries/data/mods/mod/gui/gui.rng
@@ -230,6 +230,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/binaries/data/mods/public/art/actors/special/flare_target_marker.xml b/binaries/data/mods/public/art/actors/special/flare_target_marker.xml
new file mode 100644
index 0000000000..c0450fb78b
--- /dev/null
+++ b/binaries/data/mods/public/art/actors/special/flare_target_marker.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+ skeletal/flare_target_marker.dae
+
+
+
+
+
+
+ player_trans_spec.xml
+
diff --git a/binaries/data/mods/public/art/animation/other/flare_target_marker_idle.dae b/binaries/data/mods/public/art/animation/other/flare_target_marker_idle.dae
new file mode 100644
index 0000000000..6d04649d58
--- /dev/null
+++ b/binaries/data/mods/public/art/animation/other/flare_target_marker_idle.dae
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6c34b524e89d24cbbba0dcb5d5ec8b734a16dcf4b65f4d1c73d8925ce743dbd7
+size 69619
diff --git a/binaries/data/mods/public/art/meshes/skeletal/flare_target_marker.dae b/binaries/data/mods/public/art/meshes/skeletal/flare_target_marker.dae
new file mode 100644
index 0000000000..19d8816b71
--- /dev/null
+++ b/binaries/data/mods/public/art/meshes/skeletal/flare_target_marker.dae
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee29dac3098c575c72d6c1fe2374a584181e651569377f5e0aa4e6fb6698feda
+size 69619
diff --git a/binaries/data/mods/public/art/skeletons/flare-marker-armature.xml b/binaries/data/mods/public/art/skeletons/flare-marker-armature.xml
new file mode 100644
index 0000000000..24993cb41b
--- /dev/null
+++ b/binaries/data/mods/public/art/skeletons/flare-marker-armature.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+ base
+
+
+ base
+
+ ping-marker-ball
+
+
+ ping-marker-body
+
+
+
+ target marker
+
+
+
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png
new file mode 100644
index 0000000000..e905c11608
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame00.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3bdab823affff3e81eebe9408ccec951ffc2eb734d65c4aa5cc6b0146372e40d
+size 746
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png
new file mode 100644
index 0000000000..e70d3ba848
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame01.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f425e40fab04c1caefa52412632604195af9418836b2b81b5a2bda11f33e651b
+size 1160
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png
new file mode 100644
index 0000000000..5c53cfb636
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame02.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:51b3afa3258954260b2e71ff7ad813b21673049395622a8aa847511882ecfd13
+size 1460
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png
new file mode 100644
index 0000000000..b183c68aab
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame03.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02f7659b429a29b55fb723d31cc54ab4abaf1d012d7b90827179b63d3a0cf0c6
+size 1709
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png
new file mode 100644
index 0000000000..6b6a7651c3
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame04.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:144a24887a11299db04574034c5e3b746b49816639967779db15251ca94f60a5
+size 2179
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png
new file mode 100644
index 0000000000..9400470fd9
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame05.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0aeb0eef0faa70d1ba2a68ffd3e1c2c8ea42767b834dfb66ac4e43eb95af263c
+size 2650
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png
new file mode 100644
index 0000000000..ce38244907
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame06.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81dbcd88ef3d40314510b0f472211676579d1c0a70b1907ddaa71c3260dadfbe
+size 3014
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png
new file mode 100644
index 0000000000..f815566057
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame07.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d6100d0d66caa3e025261b3568e1eec163c1dcc42a70fa808e83c2a6c1bf27cf
+size 3213
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png
new file mode 100644
index 0000000000..398e57dd82
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame08.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:16528291e07301b12652dba9eea0b530809774d13b10705d62f92239ee8a9ef7
+size 3461
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png
new file mode 100644
index 0000000000..71c5833d3e
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame09.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e73e56aa346176ac856991a6e64f37488d9cd0d736691050b8e77e95498804c2
+size 3913
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png
new file mode 100644
index 0000000000..6bb415165c
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:73377af4232cac928d881982e1550f589991ce7ee43a8c0ef22c26370be0afc7
+size 4181
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png
new file mode 100644
index 0000000000..38924985b7
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame11.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6a7c932de45dd27aca15b7239e8b2d97449e48055bfcbece07d8e493abd44bc7
+size 4356
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png
new file mode 100644
index 0000000000..65a98413bb
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame12.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2b274f6d86652ad3eeb598e0c0f377c57cc1c6aac6422d1b8db544b9a678922f
+size 4823
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png
new file mode 100644
index 0000000000..c432d521f4
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame13.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0629361330fa2b2947739cf736b4a885c4f7c03bbd0e39d7b24aed83041ec9cf
+size 5173
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png
new file mode 100644
index 0000000000..75bcd938e0
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame14.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3d7a46316d198688643d1097b1de8445faae326e879d3e9235f1e71362f20150
+size 5420
diff --git a/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png
new file mode 100644
index 0000000000..0143bd38b0
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/animated/minimap-flare/frame15.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:982819ff2a63ad94667aad3d2e1b0bbf1b8f7d95e23f4b1b24ccf908a6073598
+size 5725
diff --git a/binaries/data/mods/public/art/textures/cursors/action-flare.png b/binaries/data/mods/public/art/textures/cursors/action-flare.png
new file mode 100644
index 0000000000..6dd0eaf766
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/cursors/action-flare.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:405a2cec4de2913ed1a9513e6538f65039ddda6e658873b4913ba062de885762
+size 3519
diff --git a/binaries/data/mods/public/art/textures/cursors/action-flare.txt b/binaries/data/mods/public/art/textures/cursors/action-flare.txt
new file mode 100644
index 0000000000..2fb73a07ec
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/cursors/action-flare.txt
@@ -0,0 +1 @@
+1 1
diff --git a/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png b/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png
new file mode 100644
index 0000000000..61ff162b4e
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_diffuse.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8ac67492554c01a3b3970646bb524f8a2d6ddf985869c988d8e9bedd51964a27
+size 86587
diff --git a/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png b/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png
new file mode 100644
index 0000000000..db82364272
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/skins/skeletal/flare_target_marker_spec.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:19c3249863b82d2afe0da2673f1420410453ef79695d94c0e278dceae4eca7e9
+size 66242
diff --git a/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png b/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png
new file mode 100644
index 0000000000..c45912f3fd
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/ui/session/minimap-flare-highlight.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa589750c8619656975893f9bf14b0fbc4089a193bd01347274f7db45d4b54fb
+size 14924
diff --git a/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png b/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png
new file mode 100644
index 0000000000..6f52c95cd3
--- /dev/null
+++ b/binaries/data/mods/public/art/textures/ui/session/minimap-flare.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:655fb8c3205b7d14f77544e85f1bb8477e44d94c9f12898442aa3e451a0d9e6f
+size 6500
diff --git a/binaries/data/mods/public/gui/manual/intro.txt b/binaries/data/mods/public/gui/manual/intro.txt
index 2a27810cf6..602310963c 100644
--- a/binaries/data/mods/public/gui/manual/intro.txt
+++ b/binaries/data/mods/public/gui/manual/intro.txt
@@ -115,6 +115,7 @@ You may change hotkeys in [font="sans-bold-14"]Options > Hotkeys[font="sans-14"]
hotkey.session.attackmove + Right Click with unit(s) selected – Attack move (by default all enemy units and structures along the way are targeted)
hotkey.session.attackmoveUnit + Right Click with unit(s) selected – Attack move, only units along the way are targeted
hotkey.session.snaptoedges + Mouse Move near structures – Align the new structure with an existing nearby structure
+ hotkey.session.flare + Right Click – Send a flare to your allies
[font="sans-bold-14"]Overlays[font="sans-14"]
hotkey.session.gui.toggle – Toggle the GUI
diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js
index bbfd570be0..b0ae0e7fe2 100644
--- a/binaries/data/mods/public/gui/session/input.js
+++ b/binaries/data/mods/public/gui/session/input.js
@@ -32,6 +32,7 @@ const INPUT_BUILDING_WALL_CLICK = 8;
const INPUT_BUILDING_WALL_PATHING = 9;
const INPUT_UNIT_POSITION_START = 10;
const INPUT_UNIT_POSITION = 11;
+const INPUT_FLARE = 12;
var inputState = INPUT_NORMAL;
@@ -79,6 +80,16 @@ var g_DragStart;
*/
var clickedEntity = INVALID_ENTITY;
+/**
+ * Store the last time the flare functionality was used to prevent overusage.
+ */
+var g_LastFlareTime;
+
+/**
+ * The duration in ms for which we disable flaring after each flare to prevent overusage.
+ */
+const g_FlareCooldown = 5000;
+
// Same double-click behaviour for hotkey presses.
const doublePressTime = 500;
var doublePressTimer = 0;
@@ -89,7 +100,12 @@ function updateCursorAndTooltip()
let cursorSet = false;
let tooltipSet = false;
let informationTooltip = Engine.GetGUIObjectByName("informationTooltip");
- if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap())
+ if (inputState == INPUT_FLARE || inputState == INPUT_NORMAL && Engine.HotkeyIsPressed("session.flare") && !g_IsObserver)
+ {
+ Engine.SetCursor("action-flare");
+ cursorSet = true;
+ }
+ else if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap())
{
let action = determineAction(mouseX, mouseY, g_MiniMapPanel.isMouseOverMiniMap());
if (action)
@@ -810,6 +826,11 @@ function handleInputAfterGui(ev)
return false;
case "mousebuttondown":
+ if (Engine.HotkeyIsPressed("session.flare") && controlsPlayer(g_ViewedPlayer))
+ {
+ triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
+ return true;
+ }
if (ev.button == SDL_BUTTON_LEFT)
{
g_DragStart = new Vector2D(ev.x, ev.y);
@@ -1111,6 +1132,21 @@ function handleInputAfterGui(ev)
}
break;
+ case INPUT_FLARE:
+ if (ev.type == "mousebuttondown")
+ {
+ if (ev.button == SDL_BUTTON_LEFT && controlsPlayer(g_ViewedPlayer))
+ {
+ triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y));
+ inputState = INPUT_NORMAL;
+ return true;
+ }
+ else if (ev.button == SDL_BUTTON_RIGHT)
+ {
+ inputState = INPUT_NORMAL;
+ return true;
+ }
+ }
}
return false;
}
@@ -1224,6 +1260,21 @@ function positionUnitsFreehandSelectionMouseUp(ev)
return true;
}
+function triggerFlareAction(target)
+{
+ let now = Date.now();
+ if (g_LastFlareTime && now < g_LastFlareTime + g_FlareCooldown)
+ return;
+
+ g_LastFlareTime = now;
+ displayFlare(target, Engine.GetPlayerID());
+ Engine.PlayUISound(g_FlareSound, false);
+ Engine.PostNetworkCommand({
+ "type": "map-flare",
+ "target": target
+ });
+}
+
function handleUnitAction(target, action)
{
if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute)
diff --git a/binaries/data/mods/public/gui/session/messages.js b/binaries/data/mods/public/gui/session/messages.js
index 31311f28a3..13ebeada72 100644
--- a/binaries/data/mods/public/gui/session/messages.js
+++ b/binaries/data/mods/public/gui/session/messages.js
@@ -8,6 +8,22 @@ var g_TutorialMessages = [];
*/
var g_TutorialNewMessageTags = { "color": "255 226 149" };
+/**
+ * The number of seconds we observe for rate limiting flares.
+ */
+var g_FlareRateLimitScope = 10;
+
+/**
+ * The maximum allowed number of flares within g_FlareRateLimitScope seconds.
+ * This should be a bit larger than the number of flares that can be sent in theory by using the GUI.
+ */
+var g_FlareRateLimitMaximumFlares = 16;
+
+/**
+ * Contains the arrival timestamps the flares of the last g_FlareRateLimitScope seconds.
+ */
+var g_FlareRateLimitLastTimes = [];
+
/**
* These handlers are called everytime a client joins or disconnects.
*/
@@ -267,6 +283,27 @@ var g_NotificationsTypes =
}
global.music.setLocked(notification.lock);
+ },
+ "map-flare": function(notification, player)
+ {
+ // Don't display for the player that did the flare because they will see it immediately
+ if (player != Engine.GetPlayerID() && g_Players[player].isMutualAlly[Engine.GetPlayerID()])
+ {
+ let now = Date.now();
+ if (g_FlareRateLimitLastTimes.length)
+ {
+ g_FlareRateLimitLastTimes = g_FlareRateLimitLastTimes.filter(t => now - t < g_FlareRateLimitScope * 1000);
+ if (g_FlareRateLimitLastTimes.length >= g_FlareRateLimitMaximumFlares)
+ {
+ warn("Received too many flares. Dropping a flare request by '" + g_Players[player].name + "'.");
+ return;
+ }
+ }
+ g_FlareRateLimitLastTimes.push(now);
+
+ displayFlare(notification.target, player);
+ Engine.PlayUISound(g_FlareSound, false);
+ }
}
};
diff --git a/binaries/data/mods/public/gui/session/minimap/MiniMap.js b/binaries/data/mods/public/gui/session/minimap/MiniMap.js
index 81d5b6c8a6..2f71576056 100644
--- a/binaries/data/mods/public/gui/session/minimap/MiniMap.js
+++ b/binaries/data/mods/public/gui/session/minimap/MiniMap.js
@@ -6,9 +6,10 @@ class MiniMap
{
constructor()
{
- Engine.GetGUIObjectByName("minimap").onWorldClick = this.onWorldClick.bind(this);
- Engine.GetGUIObjectByName("minimap").onMouseEnter = this.onMouseEnter.bind(this);
- Engine.GetGUIObjectByName("minimap").onMouseLeave = this.onMouseLeave.bind(this);
+ this.miniMap = Engine.GetGUIObjectByName("minimap");
+ this.miniMap.onWorldClick = this.onWorldClick.bind(this);
+ this.miniMap.onMouseEnter = this.onMouseEnter.bind(this);
+ this.miniMap.onMouseLeave = this.onMouseLeave.bind(this);
this.mouseIsOverMiniMap = false;
}
@@ -16,9 +17,15 @@ class MiniMap
{
// Partly duplicated from handleInputAfterGui(), but with the input being
// world coordinates instead of screen coordinates.
+ if (inputState == INPUT_NORMAL && controlsPlayer(g_ViewedPlayer) && Engine.HotkeyIsPressed("session.flare"))
+ {
+ triggerFlareAction(target);
+ return true;
+ }
+
if (button == SDL_BUTTON_LEFT)
{
- if (inputState != INPUT_PRESELECTEDACTION || preSelectedAction == ACTION_NONE)
+ if (inputState != INPUT_FLARE && (inputState != INPUT_PRESELECTEDACTION || preSelectedAction == ACTION_NONE))
return false;
}
else if (button == SDL_BUTTON_RIGHT)
@@ -29,6 +36,11 @@ class MiniMap
inputState = INPUT_NORMAL;
return true;
}
+ else if (inputState == INPUT_FLARE)
+ {
+ inputState = INPUT_NORMAL;
+ return true;
+ }
else if (inputState != INPUT_NORMAL)
return false;
}
@@ -39,6 +51,13 @@ class MiniMap
if (!controlsPlayer(g_ViewedPlayer))
return false;
+ if (inputState == INPUT_FLARE && button == SDL_BUTTON_LEFT)
+ {
+ triggerFlareAction(target);
+ inputState = INPUT_NORMAL;
+ return true;
+ }
+
let action = determineAction(undefined, undefined, true);
if (!action)
return false;
@@ -64,4 +83,9 @@ class MiniMap
{
return this.mouseIsOverMiniMap;
}
+
+ flare(target, playerID)
+ {
+ return this.miniMap.flare({ "x": target.x, "y": target.z }, g_DiplomacyColors.getPlayerColor(playerID));
+ }
}
diff --git a/binaries/data/mods/public/gui/session/minimap/MiniMap.xml b/binaries/data/mods/public/gui/session/minimap/MiniMap.xml
index 4de61596bc..afe77e03d4 100644
--- a/binaries/data/mods/public/gui/session/minimap/MiniMap.xml
+++ b/binaries/data/mods/public/gui/session/minimap/MiniMap.xml
@@ -16,7 +16,7 @@
diff --git a/binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js b/binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js
new file mode 100644
index 0000000000..1bd1fbf958
--- /dev/null
+++ b/binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js
@@ -0,0 +1,29 @@
+/**
+ * If the button that this class manages is pressed, an idle unit having one of the given classes is selected.
+ */
+class MiniMapFlareButton
+{
+ constructor()
+ {
+ this.flareButton = Engine.GetGUIObjectByName("flareButton");
+ this.flareButton.onPress = this.onPress.bind(this);
+ registerHotkeyChangeHandler(this.onHotkeyChange.bind(this));
+ }
+
+ onHotkeyChange()
+ {
+ this.flareButton.tooltip =
+ colorizeHotkey("%(hotkey)s" + " ", "session.flare") +
+ translate(this.Tooltip);
+ }
+
+ onPress()
+ {
+ if (g_IsObserver)
+ return;
+ if (inputState == INPUT_NORMAL)
+ inputState = INPUT_FLARE;
+ }
+}
+
+MiniMapFlareButton.prototype.Tooltip = markForTranslation("Send a flare to your allies");
diff --git a/binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js b/binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js
index 8b1ee2e9c2..effcbc77b9 100644
--- a/binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js
+++ b/binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js
@@ -7,9 +7,15 @@ class MiniMapPanel
{
this.diplomacyColorsButton = new MiniMapDiplomacyColorsButton(diplomacyColors);
this.idleWorkerButton = new MiniMapIdleWorkerButton(playerViewControl, idleWorkerClasses);
+ this.flareButton = new MiniMapFlareButton();
this.miniMap = new MiniMap();
}
+ flare(target, playerID)
+ {
+ return this.miniMap.flare(target, playerID);
+ }
+
isMouseOverMiniMap()
{
return this.miniMap.isMouseOverMiniMap();
diff --git a/binaries/data/mods/public/gui/session/unit_actions.js b/binaries/data/mods/public/gui/session/unit_actions.js
index 9676911c4e..6393120bdd 100644
--- a/binaries/data/mods/public/gui/session/unit_actions.js
+++ b/binaries/data/mods/public/gui/session/unit_actions.js
@@ -3,9 +3,15 @@
* given a command type.
*/
var g_TargetMarker = {
- "move": "special/target_marker"
+ "move": "special/target_marker",
+ "map_flare": "special/flare_target_marker"
};
+/**
+ * Sound we play when displaying a flare.
+ */
+var g_FlareSound = "audio/interface/alarm/alarmally_1.ogg";
+
/**
* Which enemy entity types will be attacked on sight when patroling.
*/
@@ -1828,6 +1834,17 @@ function DrawTargetMarker(target)
});
}
+function displayFlare(target, playerID)
+{
+ Engine.GuiInterfaceCall("AddTargetMarker", {
+ "template": g_TargetMarker.map_flare,
+ "x": target.x,
+ "z": target.z,
+ "owner": playerID
+ });
+ g_MiniMapPanel.flare(target, playerID);
+}
+
function getCommandInfo(command, entStates)
{
return entStates && g_EntityCommands[command] &&
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index 762ff81eb1..62b2d028f1 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -1116,7 +1116,9 @@ GuiInterface.prototype.AddTargetMarker = function(player, cmd)
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
-
+ let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpOwnership)
+ cmpOwnership.SetOwner(cmd.owner);
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js
index 5f672c99ab..cd468bdeb2 100644
--- a/binaries/data/mods/public/simulation/helpers/Commands.js
+++ b/binaries/data/mods/public/simulation/helpers/Commands.js
@@ -878,6 +878,16 @@ var g_Commands = {
}
},
+ "map-flare": function(player, cmd, data)
+ {
+ let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ cmpGuiInterface.PushNotification({
+ "type": "map-flare",
+ "players": [player],
+ "target": cmd.target
+ });
+ },
+
"autoqueue-on": function(player, cmd, data)
{
for (let ent of data.entities)
diff --git a/binaries/data/mods/public/simulation/templates/special/flare_target_marker.xml b/binaries/data/mods/public/simulation/templates/special/flare_target_marker.xml
new file mode 100644
index 0000000000..89387588ed
--- /dev/null
+++ b/binaries/data/mods/public/simulation/templates/special/flare_target_marker.xml
@@ -0,0 +1,17 @@
+
+
+
+ true
+ false
+ 6
+ 10000
+ 0
+
+
+
+ true
+
+
+ special/flare_target_marker.xml
+
+
diff --git a/source/gui/GUIObjectTypes.cpp b/source/gui/GUIObjectTypes.cpp
index 91200a77f4..54b52dee72 100644
--- a/source/gui/GUIObjectTypes.cpp
+++ b/source/gui/GUIObjectTypes.cpp
@@ -40,6 +40,7 @@ void CGUI::AddObjectTypes()
m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface));
m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface));
m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface));
+ m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface));
m_ProxyData.insert(JSI_GUIProxy::CreateData(*m_ScriptInterface));
AddObjectType("button", &CButton::ConstructObject);
diff --git a/source/gui/ObjectTypes/CMiniMap.cpp b/source/gui/ObjectTypes/CMiniMap.cpp
index b238b13f83..e285c3a969 100644
--- a/source/gui/ObjectTypes/CMiniMap.cpp
+++ b/source/gui/ObjectTypes/CMiniMap.cpp
@@ -37,6 +37,7 @@
#include "lib/external_libraries/libsdl.h"
#include "lib/ogl.h"
#include "lib/timer.h"
+#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/CStrInternStatic.h"
#include "ps/Filesystem.h"
@@ -141,7 +142,10 @@ CMiniMap::CMiniMap(CGUI& pGUI) :
IGUIObject(pGUI),
m_MapSize(0), m_MapScale(1.f),
m_EntitiesDrawn(0), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW), m_Mask(this, "mask", false),
- m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false)
+ m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false),
+ m_FlareTextureCount(this, "flare_texture_count", 0), m_FlareRenderSize(this, "flare_render_size", 0),
+ m_FlareAnimationSpeed(this, "flare_animation_speed", 0), m_FlareInterleave(this, "flare_interleave", false),
+ m_FlareLifetimeSeconds(this, "flare_lifetime_seconds", 0)
{
m_Clicking = false;
m_MouseHovering = false;
@@ -201,6 +205,13 @@ void CMiniMap::HandleMessage(SGUIMessage& Message)
IGUIObject::HandleMessage(Message);
switch (Message.type)
{
+ case GUIM_LOAD:
+ RecreateFlareTextures();
+ break;
+ case GUIM_SETTINGS_UPDATED:
+ if (Message.value == "flare_texture_count")
+ RecreateFlareTextures();
+ break;
case GUIM_MOUSE_PRESS_LEFT:
if (m_MouseHovering)
{
@@ -248,6 +259,24 @@ void CMiniMap::HandleMessage(SGUIMessage& Message)
}
}
+void CMiniMap::RecreateFlareTextures()
+{
+ // Catch invalid values.
+ if (m_FlareTextureCount > 99)
+ {
+ LOGERROR("Invalid value for flare texture count. Valid range is 0-99.");
+ return;
+ }
+ const CStr numberingFormat = "%02u";
+ m_FlareTextures.clear();
+ m_FlareTextures.reserve(m_FlareTextureCount);
+ for (u32 i = 0; i < m_FlareTextureCount; ++i)
+ {
+ CTextureProperties textureProps(L"art/textures/animated/minimap-flare/frame" + CStr(fmt::sprintf(numberingFormat, i)).FromUTF8() + L".png");
+ m_FlareTextures.emplace_back(g_Renderer.GetTextureManager().CreateTexture(textureProps));
+ }
+}
+
bool CMiniMap::IsMouseOver() const
{
// Get the mouse position.
@@ -369,6 +398,41 @@ void CMiniMap::DrawViewRect(const CMatrix3D& transform) const
glLineWidth(1.0f);
}
+void CMiniMap::DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double currentTime) const
+{
+ if (!m_FlareTextures.size())
+ return;
+
+ // Coordinates with 0,0 in the middle of the minimap and +-0.5 as max.
+ const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize);
+ const float relativeX = (flare.pos.X * invTileMapSize - 0.5) / m_MapScale;
+ const float relativeY = (flare.pos.Y * invTileMapSize - 0.5) / m_MapScale;
+
+ // Rotate coordinates.
+ const float angle = GetAngle();
+ const float rotatedX = cos(angle) * relativeX + sin(angle) * relativeY;
+ const float rotatedY = -sin(angle) * relativeX + cos(angle) * relativeY;
+ // Calculate coordinates in gui space.
+ const float cx = m_CachedActualSize.left + (0.5f + rotatedX) * m_CachedActualSize.GetWidth();
+ const float cy = m_CachedActualSize.bottom - (0.5f + rotatedY) * m_CachedActualSize.GetHeight();
+
+ const CRect destination(cx-m_FlareRenderSize, cy-m_FlareRenderSize, cx+m_FlareRenderSize, cy+m_FlareRenderSize);
+
+ const u32 flooredStep = floor((currentTime - flare.time) * m_FlareAnimationSpeed);
+
+ CTexturePtr texture = m_FlareTextures[flooredStep % m_FlareTextures.size()];
+ // TODO: Only draw inside the minimap circle.
+ canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), flare.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f);
+
+ // Draw a second circle if the first has reached half of the animation
+ if (m_FlareInterleave && flooredStep >= m_FlareTextures.size() / 2)
+ {
+ texture = m_FlareTextures[(flooredStep - m_FlareTextures.size() / 2) % m_FlareTextures.size()];
+ // TODO: Only draw inside the minimap circle.
+ canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), flare.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f);
+ }
+}
+
struct MinimapUnitVertex
{
// This struct is copyable for convenience and because to move is to copy for primitives.
@@ -521,7 +585,7 @@ void CMiniMap::Draw(CCanvas2D& canvas)
glDisable(GL_BLEND);
- PROFILE_START("minimap units");
+ PROFILE_START("minimap units and flares");
CShaderDefines pointDefines;
pointDefines.Add(str_MINIMAP_POINT, str_1);
@@ -641,5 +705,23 @@ void CMiniMap::Draw(CCanvas2D& canvas)
DrawViewRect(unitMatrix);
- PROFILE_END("minimap units");
+ while (!m_MapFlares.empty() && m_FlareLifetimeSeconds + m_MapFlares.front().time < cur_time)
+ m_MapFlares.pop_front();
+
+ for (const MapFlare& flare : m_MapFlares)
+ DrawFlare(canvas, flare, cur_time);
+
+ PROFILE_END("minimap units and flares");
+}
+
+bool CMiniMap::Flare(const CVector2D& pos, const CStr& colorStr)
+{
+ CColor color;
+ if (!color.ParseString(colorStr))
+ {
+ LOGERROR("CMiniMap::Flare: Couldn't parse color string");
+ return false;
+ }
+ m_MapFlares.push_back({ pos, color, timer_Time() });
+ return true;
}
diff --git a/source/gui/ObjectTypes/CMiniMap.h b/source/gui/ObjectTypes/CMiniMap.h
index 910b474cbc..c341e602ae 100644
--- a/source/gui/ObjectTypes/CMiniMap.h
+++ b/source/gui/ObjectTypes/CMiniMap.h
@@ -18,9 +18,15 @@
#ifndef INCLUDED_MINIMAP
#define INCLUDED_MINIMAP
+#include "graphics/Color.h"
+#include "graphics/Texture.h"
#include "gui/ObjectBases/IGUIObject.h"
+#include "maths/Vector2D.h"
#include "renderer/VertexArray.h"
+#include
+#include
+
class CCamera;
class CMatrix3D;
class CTerrain;
@@ -28,13 +34,25 @@ class CTerrain;
class CMiniMap : public IGUIObject
{
GUI_OBJECT(CMiniMap)
+
public:
CMiniMap(CGUI& pGUI);
virtual ~CMiniMap();
+ bool Flare(const CVector2D& pos, const CStr& colorStr);
+
protected:
+ struct MapFlare
+ {
+ CVector2D pos;
+ CColor color;
+ double time;
+ };
+
virtual void Draw(CCanvas2D& canvas);
+ virtual void CreateJSObject();
+
/**
* @see IGUIObject#HandleMessage()
*/
@@ -57,6 +75,16 @@ private:
// Whether or not the mouse is currently down
bool m_Clicking;
+ std::deque m_MapFlares;
+
+ std::vector m_FlareTextures;
+
+ CGUISimpleSetting m_FlareTextureCount;
+ CGUISimpleSetting m_FlareRenderSize;
+ CGUISimpleSetting m_FlareAnimationSpeed;
+ CGUISimpleSetting m_FlareInterleave;
+ CGUISimpleSetting m_FlareLifetimeSeconds;
+
// Whether to draw a black square around and under the minimap.
CGUISimpleSetting m_Mask;
@@ -66,8 +94,12 @@ private:
// 1.f if map is circular or 1.414f if square (to shrink it inside the circle)
float m_MapScale;
+ void RecreateFlareTextures();
+
void DrawViewRect(const CMatrix3D& transform) const;
+ void DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double curentTime) const;
+
void GetMouseWorldCoordinates(float& x, float& z) const;
float GetAngle() const;
diff --git a/source/gui/Scripting/JSInterface_GUIProxy.cpp b/source/gui/Scripting/JSInterface_GUIProxy.cpp
index b500e08be5..f5552b08ff 100644
--- a/source/gui/Scripting/JSInterface_GUIProxy.cpp
+++ b/source/gui/Scripting/JSInterface_GUIProxy.cpp
@@ -22,6 +22,7 @@
#include "gui/ObjectBases/IGUIObject.h"
#include "gui/ObjectTypes/CButton.h"
#include "gui/ObjectTypes/CList.h"
+#include "gui/ObjectTypes/CMiniMap.h"
#include "gui/ObjectTypes/CText.h"
// Called for every specialization - adds the common interface.
@@ -58,3 +59,10 @@ template<> void JSI_GUIProxy::CreateFunctions(const ScriptRequest& rq, GU
CreateFunction(&CList::AddItem)>(rq, cache, "addItem");
}
DECLARE_GUIPROXY(CList);
+
+// CMiniMap
+template<> void JSI_GUIProxy::CreateFunctions(const ScriptRequest& rq, GUIProxyProps* cache)
+{
+ CreateFunction<&CMiniMap::Flare>(rq, cache, "flare");
+}
+DECLARE_GUIPROXY(CMiniMap);