Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -320,6 +320,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 [hotkey.session.gui] toggle = "Alt+G" ; Toggle visibility of session GUI 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 @@ -116,6 +116,7 @@ • Otherwise – Attack move (by default all enemy units and structures along the way are targeted) Ctrl + Q + Right Click with unit(s) selected – Attack move, only units along the way are targeted Ctrl + 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"] Alt + G – Hide/show 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; } @@ -1230,6 +1245,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-ping", + "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 @@ -271,6 +271,17 @@ } global.music.setLocked(notification.lock); + }, + "map-ping": function(notification, player) + { + // Don't display for the player that did the ping 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/ui/chat_alert.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 @@ -36,4 +36,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_ping": "special/target_marker" }; /** @@ -1570,6 +1572,16 @@ }); } +function displayFlare(target, playerID) +{ + Engine.GuiInterfaceCall("AddTargetMarker", { + "template": g_TargetMarker.map_ping, + "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 @@ -9,7 +9,7 @@ varying vec3 color; #endif -#if MINIMAP_LINE +#if MINIMAP_LINE || MINIMAP_FLARE uniform vec4 color; #endif @@ -27,6 +27,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 @@ -4,7 +4,7 @@ uniform mat4 textureTransform; uniform float pointSize; -#if MINIMAP_BASE || MINIMAP_LOS +#if MINIMAP_BASE || MINIMAP_LOS || MINIMAP_FLARE attribute vec3 a_vertex; attribute vec2 a_uv0; varying vec2 v_tex; @@ -22,7 +22,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 @@ -845,6 +845,16 @@ cmpResourceDropsite.SetSharing(cmd.shared); } }, + + "map-ping": function(player, cmd, data) + { + let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({ + "type": "map-ping", + "players": [player], + "target": cmd.target + }); + }, }; /** Index: source/gui/ObjectTypes/CMiniMap.h =================================================================== --- source/gui/ObjectTypes/CMiniMap.h +++ source/gui/ObjectTypes/CMiniMap.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -19,6 +19,7 @@ #define INCLUDED_MINIMAP #include "graphics/ShaderProgramPtr.h" +#include "graphics/Texture.h" #include "gui/ObjectBases/IGUIObject.h" #include "renderer/VertexArray.h" @@ -26,6 +27,13 @@ class CMatrix3D; class CTerrain; +struct MapFlareObj +{ + CPos pos; + CColor color; + double time; +}; + class CMiniMap : public IGUIObject { GUI_OBJECT(CMiniMap) @@ -35,12 +43,22 @@ protected: virtual void Draw(); + virtual void RegisterScriptFunctions(); + /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** + * Script accessors to this GUI object. + */ + static JSFunctionSpec JSI_methods[]; + + static bool FlareProxy(JSContext* cx, uint argc, JS::Value* vp); + bool Flare(JSContext* cx, uint argc, JS::Value* vp); + + /** * @see IGUIObject#IsMouseOver() */ virtual bool IsMouseOver() const; @@ -68,9 +86,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; @@ -95,10 +117,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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -33,6 +33,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/Filesystem.h" #include "ps/Game.h" @@ -134,6 +135,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() @@ -224,6 +231,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(); @@ -263,14 +278,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); @@ -278,19 +292,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. @@ -322,6 +333,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. @@ -346,7 +405,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) @@ -490,7 +548,7 @@ glDisable(GL_BLEND); - PROFILE_START("minimap units"); + PROFILE_START("minimap units and flares"); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); @@ -612,7 +670,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); @@ -708,6 +794,59 @@ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_MapSize - 1, m_MapSize - 1, GL_RGBA, GL_UNSIGNED_BYTE, m_TerrainData); } +JSFunctionSpec CMiniMap::JSI_methods[] = +{ + JS_FN("flare", CMiniMap::FlareProxy, 2, 0), + JS_FS_END +}; + +void CMiniMap::RegisterScriptFunctions() +{ + JSContext* cx = m_pGUI.GetScriptInterface()->GetContext(); + JSAutoRequest rq(cx); + JS_DefineFunctions(cx, m_JSObject, JSI_methods); +} + +bool CMiniMap::FlareProxy(JSContext* cx, uint argc, JS::Value* vp) +{ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + CMiniMap* thisObj = ScriptInterface::GetPrivate(cx, args, &JSI_IGUIObject::JSI_class); + if (!thisObj) + { + JSAutoRequest rq(cx); + JS_ReportError(cx, "This is not a CMiniMap object!"); + return false; + } + + bool ret = thisObj->Flare(cx, argc, vp); + + args.rval().set(JS::BooleanValue(ret)); + return true; +} + +bool CMiniMap::Flare(JSContext* cx, uint argc, JS::Value* vp) +{ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + bool ret = false; + if (args.length() != 2) + LOGERROR("CMiniMap::Flare: Got %d arguments, but expected 2.", args.length()); + else + { + JSAutoRequest rq(cx); + CVector2D pos; + CStr colorStr; + CColor color; + if (ScriptInterface::FromJSVal(cx, args.get(0), pos) && + ScriptInterface::FromJSVal(cx, args.get(1), colorStr) && + color.ParseString(colorStr)) + { + m_MapFlares.push_back({GetMapCoordinates(pos.X, pos.Y), color, timer_Time()}); + ret = true; + } + } + return ret; +} + void CMiniMap::Destroy() { if (m_TerrainTexture) Index: source/ps/CStrInternStatic.h =================================================================== --- source/ps/CStrInternStatic.h +++ source/ps/CStrInternStatic.h @@ -42,6 +42,7 @@ X(MINIMAP_LINE) X(MINIMAP_LOS) X(MINIMAP_POINT) +X(MINIMAP_FLARE) X(MODE_SHADOWCAST) X(MODE_SILHOUETTEDISPLAY) X(MODE_SILHOUETTEOCCLUDER)