Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -309,6 +309,7 @@ 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 +flare = K ; Modifier to send a flare to your allies ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel Index: binaries/data/mods/public/art/textures/cursors/action-flare.txt =================================================================== --- /dev/null +++ binaries/data/mods/public/art/textures/cursors/action-flare.txt @@ -0,0 +1 @@ +1 1 Index: binaries/data/mods/public/gui/manual/intro.txt =================================================================== --- binaries/data/mods/public/gui/manual/intro.txt +++ binaries/data/mods/public/gui/manual/intro.txt @@ -114,6 +114,7 @@ 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 + K + Right Click – Send a flare to your allies [font="sans-bold-14"]Overlays[font="sans-14"] hotkey.session.gui.toggle – Toggle the GUI Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -17,6 +17,7 @@ const ACTION_REPAIR = 2; const ACTION_GUARD = 3; const ACTION_PATROL = 4; +const ACTION_FLARE = 5; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; @@ -78,6 +79,16 @@ */ 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 = 1000; + // Same double-click behaviour for hotkey presses const doublePressTime = 500; var doublePressTimer = 0; @@ -208,6 +219,10 @@ */ function determineAction(x, y, fromMiniMap) { + let r = g_MiniMapPanel.preSelectedActionCheck() || g_MiniMapPanel.hotkeyActionCheck(); + if (r) + return r; + let selection = g_Selection.toList(); if (!selection.length) { @@ -244,7 +259,7 @@ for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].preSelectedActionCheck) { - let r = g_UnitActions[action].preSelectedActionCheck(target, selection); + r = g_UnitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } @@ -1251,6 +1266,20 @@ function handleUnitAction(target, action) { + if (action.type == "flare") + { + let now = Date.now(); + if (g_LastFlareTime && now < g_LastFlareTime + g_FlareCooldown) + return false; + + g_LastFlareTime = now; + displayFlare(target, Engine.GetPlayerID()); + Engine.PostNetworkCommand({ + "type": "map-flare", + "target": target + }); + return true; + } if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute) { error("Invalid action.type " + action.type); Index: binaries/data/mods/public/gui/session/messages.js =================================================================== --- binaries/data/mods/public/gui/session/messages.js +++ binaries/data/mods/public/gui/session/messages.js @@ -265,6 +265,17 @@ } 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()]) + { + displayFlare(notification.target, player); + + // TODO: Create a new sound and play it here + Engine.PlayUISound("audio/interface/alarm/alarmally_1.ogg", false); + } } }; Index: binaries/data/mods/public/gui/session/minimap/MiniMap.js =================================================================== --- binaries/data/mods/public/gui/session/minimap/MiniMap.js +++ binaries/data/mods/public/gui/session/minimap/MiniMap.js @@ -6,9 +6,10 @@ { 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; } @@ -64,4 +65,9 @@ { return this.mouseIsOverMiniMap; } + + flare(target, playerID) + { + return this.miniMap.flare([target.x, target.z], g_DiplomacyColors.getPlayerColor(playerID)); + } } Index: binaries/data/mods/public/gui/session/minimap/MiniMap.xml =================================================================== --- binaries/data/mods/public/gui/session/minimap/MiniMap.xml +++ binaries/data/mods/public/gui/session/minimap/MiniMap.xml @@ -33,4 +33,14 @@ tooltip_style="sessionToolTip" hotkey="session.diplomacycolors" /> + + + Index: binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/minimap/MiniMapFlareButton.js @@ -0,0 +1,50 @@ +/** + * 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() + { + inputState = INPUT_PRESELECTEDACTION; + preSelectedAction = ACTION_FLARE; + } + + getAction(target) + { + return { + "type": "flare", + "cursor": "action-flare", + "target": target + }; + } + + hotkeyActionCheck(target, selection) + { + if (!Engine.HotkeyIsPressed("session.flare")) + return false; + return this.getAction(target); + } + + preSelectedActionCheck(target, selection) + { + if (preSelectedAction != ACTION_FLARE) + return false; + return this.getAction(target); + } +} + +MiniMapFlareButton.prototype.Tooltip = markForTranslation("Send a flare to your allies"); Index: binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js =================================================================== --- binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js +++ binaries/data/mods/public/gui/session/minimap/MiniMapPanel.js @@ -7,9 +7,25 @@ { this.diplomacyColorsButton = new MiniMapDiplomacyColorsButton(diplomacyColors); this.idleWorkerButton = new MiniMapIdleWorkerButton(playerViewControl, idleWorkerClasses); + this.flareButton = new MiniMapFlareButton(); this.miniMap = new MiniMap(); } + hotkeyActionCheck(target, selection) + { + return this.flareButton.hotkeyActionCheck(target, selection); + } + + preSelectedActionCheck(target, selection) + { + return this.flareButton.preSelectedActionCheck(target, selection); + } + + flare(target, playerID) + { + return this.miniMap.flare(target, playerID); + } + isMouseOverMiniMap() { return this.miniMap.isMouseOverMiniMap(); Index: binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- binaries/data/mods/public/gui/session/unit_actions.js +++ binaries/data/mods/public/gui/session/unit_actions.js @@ -3,7 +3,9 @@ * given a command type. */ var g_TargetMarker = { - "move": "special/target_marker" + "move": "special/target_marker", + // TODO: Add proper marker + "map_flare": "special/target_marker" }; /** @@ -1611,6 +1613,16 @@ }); } +function displayFlare(target, playerID) +{ + Engine.GuiInterfaceCall("AddTargetMarker", { + "template": g_TargetMarker.map_flare, + "x": target.x, + "z": target.z + }); + g_MiniMapPanel.flare(target, playerID) +} + function findGatherType(gatherer, supply) { if (!gatherer.resourceGatherRates || !supply) Index: binaries/data/mods/public/shaders/glsl/minimap.fs =================================================================== --- binaries/data/mods/public/shaders/glsl/minimap.fs +++ binaries/data/mods/public/shaders/glsl/minimap.fs @@ -1,6 +1,6 @@ #version 110 -#if MINIMAP_BASE || MINIMAP_LOS +#if MINIMAP_BASE || MINIMAP_LOS || MINIMAP_FLARE uniform sampler2D baseTex; varying vec2 v_tex; #endif @@ -14,7 +14,7 @@ varying vec3 color; #endif -#if MINIMAP_LINE +#if MINIMAP_LINE || MINIMAP_FLARE uniform vec4 color; #endif @@ -42,6 +42,10 @@ gl_FragColor = vec4(color, 1.0); #endif + #if MINIMAP_FLARE + gl_FragColor = texture2D(baseTex, v_tex) * color; + #endif + #if MINIMAP_LINE gl_FragColor = color; #endif Index: binaries/data/mods/public/shaders/glsl/minimap.vs =================================================================== --- binaries/data/mods/public/shaders/glsl/minimap.vs +++ binaries/data/mods/public/shaders/glsl/minimap.vs @@ -9,7 +9,7 @@ varying vec2 v_maskUV; #endif -#if MINIMAP_BASE || MINIMAP_LOS || MINIMAP_MASK +#if MINIMAP_BASE || MINIMAP_LOS || MINIMAP_MASK || MINIMAP_FLARE attribute vec3 a_vertex; attribute vec2 a_uv0; #endif @@ -30,7 +30,7 @@ void main() { - #if MINIMAP_BASE || MINIMAP_LOS + #if MINIMAP_BASE || MINIMAP_LOS || MINIMAP_FLARE gl_Position = transform * vec4(a_vertex, 1.0); v_tex = (textureTransform * vec4(a_uv0, 0.0, 1.0)).xy; #endif Index: binaries/data/mods/public/shaders/glsl/minimap.xml =================================================================== --- binaries/data/mods/public/shaders/glsl/minimap.xml +++ binaries/data/mods/public/shaders/glsl/minimap.xml @@ -6,7 +6,7 @@ - + Index: binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Commands.js +++ binaries/data/mods/public/simulation/helpers/Commands.js @@ -828,6 +828,16 @@ cmpResourceDropsite.SetSharing(cmd.shared); } }, + + "map-flare": function(player, cmd, data) + { + let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({ + "type": "map-flare", + "players": [player], + "target": cmd.target + }); + }, }; /** Index: source/gui/ObjectTypes/CMiniMap.h =================================================================== --- source/gui/ObjectTypes/CMiniMap.h +++ source/gui/ObjectTypes/CMiniMap.h @@ -19,16 +19,27 @@ #define INCLUDED_MINIMAP #include "graphics/ShaderProgramPtr.h" +#include "graphics/Texture.h" #include "gui/ObjectBases/IGUIObject.h" #include "renderer/VertexArray.h" class CCamera; class CMatrix3D; class CTerrain; +class CVector2D; + +struct MapFlareObj +{ + CPos pos; + CColor color; + double time; +}; class CMiniMap : public IGUIObject { GUI_OBJECT(CMiniMap) + + friend JSI_GUIProxy; public: CMiniMap(CGUI& pGUI); virtual ~CMiniMap(); @@ -37,16 +48,24 @@ * @return The maximum height for unit passage in water. */ static float GetShallowPassageHeight(); + bool Flare(const CVector2D& pos, const CColor& color); protected: virtual void Draw(); + virtual void CreateJSObject(); + /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** + * Script accessors to this GUI object. + */ + static JSFunctionSpec JSI_methods[]; + + /** * @see IGUIObject#IsMouseOver() */ virtual bool IsMouseOver() const; @@ -74,9 +93,13 @@ //Whether or not the mouse is currently down bool m_Clicking; + std::deque m_MapFlares; + // minimap texture handles GLuint m_TerrainTexture; + CTexturePtr m_FlareTextures[16]; + // texture data u32* m_TerrainData; @@ -104,10 +127,14 @@ void DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) const; - void DrawViewRect(CMatrix3D transform) const; + void DrawViewRect(const CMatrix3D& transform) const; + + void DrawFlare(CShaderProgramPtr shader, const MapFlareObj& flare, double cur_time) const; void GetMouseWorldCoordinates(float& x, float& z) const; + CPos GetMapCoordinates(float x, float z) const; + float GetAngle() const; VertexIndexArray m_IndexArray; Index: source/gui/ObjectTypes/CMiniMap.cpp =================================================================== --- source/gui/ObjectTypes/CMiniMap.cpp +++ source/gui/ObjectTypes/CMiniMap.cpp @@ -29,10 +29,12 @@ #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/GUIMatrix.h" +#include "gui/Scripting/JSInterface_GUIProxy.h" #include "lib/bits.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/timer.h" +#include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" @@ -131,6 +133,12 @@ CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration); } m_HalfBlinkDuration = blinkDuration/2; + + for (int i = 0; i < 16; ++i) + { + CTextureProperties textureProps(L"art/textures/animated/minimap-flare/frame" + CStrW::FromInt(i) + L".png"); + m_FlareTextures[i] = g_Renderer.GetTextureManager().CreateTexture(textureProps); + } } CMiniMap::~CMiniMap() @@ -221,6 +229,14 @@ z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5); } +CPos CMiniMap::GetMapCoordinates(float x, float z) const +{ + const float width = m_CachedActualSize.GetWidth(); + const float height = m_CachedActualSize.GetHeight(); + const float invTileMapSize = 1.0f / (TERRAIN_TILE_SIZE * m_MapSize); + return CPos(width * x * invTileMapSize, height * z * invTileMapSize); +} + void CMiniMap::SetCameraPos() { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); @@ -259,14 +275,13 @@ // This sets up and draws the rectangle on the minimap // which represents the view of the camera in the world. -void CMiniMap::DrawViewRect(CMatrix3D transform) const +void CMiniMap::DrawViewRect(const CMatrix3D& transform) const { // Compute the camera frustum intersected with a fixed-height plane. // Use the water height as a fixed base height, which should be the lowest we can go float h = g_Renderer.GetWaterManager()->m_WaterHeight; const float width = m_CachedActualSize.GetWidth(); const float height = m_CachedActualSize.GetHeight(); - const float invTileMapSize = 1.0f / float(TERRAIN_TILE_SIZE * m_MapSize); CVector3D hitPt[4]; hitPt[0] = m_Camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), h); @@ -274,19 +289,16 @@ hitPt[2] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, h); hitPt[3] = m_Camera->GetWorldCoordinates(0, 0, h); - float ViewRect[4][2]; + CPos ViewRect[4]; for (int i = 0; i < 4; ++i) - { // convert to minimap space - ViewRect[i][0] = (width * hitPt[i].X * invTileMapSize); - ViewRect[i][1] = (height * hitPt[i].Z * invTileMapSize); - } + ViewRect[i] = GetMapCoordinates(hitPt[i].X, hitPt[i].Z); float viewVerts[] = { - ViewRect[0][0], -ViewRect[0][1], - ViewRect[1][0], -ViewRect[1][1], - ViewRect[2][0], -ViewRect[2][1], - ViewRect[3][0], -ViewRect[3][1] + ViewRect[0].x, -ViewRect[0].y, + ViewRect[1].x, -ViewRect[1].y, + ViewRect[2].x, -ViewRect[2].y, + ViewRect[3].x, -ViewRect[3].y }; // Enable Scissoring to restrict the rectangle to only the minimap. @@ -318,6 +330,54 @@ glDisable(GL_SCISSOR_TEST); } +void CMiniMap::DrawFlare(CShaderProgramPtr shader, const MapFlareObj& flare, double cur_time) const +{ + float step = 35;//std::fmod((cur_time - flare.time) * 15, 35); + float x = flare.pos.x + step; + float y = -flare.pos.y-step; + float x2 = flare.pos.x - step; + float y2 = -flare.pos.y+step; + float z = 0; + + float quadTex[] = { + 0, 1, + 1, 1, + 1, 0, + + 1, 0, + 0, 0, + 0, 1 + }; + float quadVerts[] = { + x, y, z, + x2, y, z, + x2, y2, z, + + x2, y2, z, + x, y2, z, + x, y, z + }; + + int flooredStep = floor((cur_time - flare.time) * 16); + + shader->Uniform(str_color, flare.color); + shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex); + shader->VertexPointer(3, GL_FLOAT, 0, quadVerts); + shader->AssertPointersBound(); + + if (g_Renderer.m_SkipSubmit) + return; + + shader->BindTexture(str_baseTex, m_FlareTextures[flooredStep % 16]); + glDrawArrays(GL_TRIANGLES, 0, 6); + + if (flooredStep >= 8) + { + shader->BindTexture(str_baseTex, m_FlareTextures[(flooredStep - 8) % 16]); + glDrawArrays(GL_TRIANGLES, 0, 6); + } +} + struct MinimapUnitVertex { // This struct is copyable for convenience and because to move is to copy for primitives. @@ -342,7 +402,6 @@ ++attrPos; } - void CMiniMap::DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) const { // Rotate the texture coordinates (0,0)-(coordMax,coordMax) around their center point (m,m) @@ -513,7 +572,7 @@ glDisable(GL_BLEND); - PROFILE_START("minimap units"); + PROFILE_START("minimap units and flares"); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); @@ -635,7 +694,35 @@ DrawViewRect(unitMatrix); - PROFILE_END("minimap units"); + while (!m_MapFlares.empty() && 6 + m_MapFlares.front().time < cur_time) + m_MapFlares.pop_front(); + + glEnable(GL_BLEND); + + const float width = m_CachedActualSize.GetWidth(); + const float height = m_CachedActualSize.GetHeight(); + glScissor( + m_CachedActualSize.left * g_GuiScale, + g_Renderer.GetHeight() - m_CachedActualSize.bottom * g_GuiScale, + width * g_GuiScale, + height * g_GuiScale); + glEnable(GL_SCISSOR_TEST); + + CShaderDefines flareDefines; + flareDefines.Add(str_MINIMAP_FLARE, str_1); + tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), flareDefines); + tech->BeginPass(); + shader = tech->GetShader(); + + shader->Uniform(str_transform, unitMatrix); + shader->Uniform(str_textureTransform, baseTextureTransform); + for (const MapFlareObj& flare : m_MapFlares) + DrawFlare(shader, flare, cur_time); + tech->EndPass(); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + + PROFILE_END("minimap units and flares"); // Reset depth mask glDepthMask(1); @@ -731,6 +818,26 @@ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_MapSize - 1, m_MapSize - 1, GL_RGBA, GL_UNSIGNED_BYTE, m_TerrainData); } +bool CMiniMap::Flare(const CVector2D& pos, const CColor& color) +{ + m_MapFlares.push_back({GetMapCoordinates(pos.X, pos.Y), color, timer_Time()}); + return true; +} + +void CMiniMap::CreateJSObject() +{ + ScriptRequest rq(m_pGUI.GetScriptInterface()); + + js::ProxyOptions options; + options.setClass(&JSI_GUIProxy::ClassDefinition()); + + JS::RootedValue cppObj(rq.cx), data(rq.cx); + cppObj.get().setPrivate(this); + data.get().setPrivate(GetGUI().GetProxyData(&JSI_GUIProxy::Singleton())); + m_JSObject.init(rq.cx, js::NewProxyObject(rq.cx, &JSI_GUIProxy::Singleton(), cppObj, nullptr, options)); + js::SetProxyReservedSlot(m_JSObject, 0, data); +} + void CMiniMap::Destroy() { if (m_TerrainTexture) Index: source/ps/CStrInternStatic.h =================================================================== --- source/ps/CStrInternStatic.h +++ source/ps/CStrInternStatic.h @@ -43,6 +43,7 @@ X(MINIMAP_LOS) X(MINIMAP_MASK) X(MINIMAP_POINT) +X(MINIMAP_FLARE) X(MODE_SHADOWCAST) X(MODE_SILHOUETTEDISPLAY) X(MODE_SILHOUETTEOCCLUDER)