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);