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,12 @@ } 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); } }; 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,19 @@ }); } +function displayFlare(target, playerID) +{ + Engine.GuiInterfaceCall("AddTargetMarker", { + "template": g_TargetMarker.map_ping, + "x": target.x, + "z": target.z + }); + g_MiniMapPanel.flare(target, playerID) + + // TODO: Create a new sound and play it here + Engine.PlayUISound("audio/interface/ui/chat_alert.ogg", false); +} + function findGatherType(gatherer, supply) { if (!gatherer.resourceGatherRates || !supply) 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 @@ -26,6 +26,13 @@ class CMatrix3D; class CTerrain; +struct MapFlareObj +{ + CPos pos; + CColor color; + double time; +}; + class CMiniMap : public IGUIObject { GUI_OBJECT(CMiniMap) @@ -35,12 +42,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,6 +85,8 @@ //Whether or not the mouse is currently down bool m_Clicking; + std::deque m_MapFlares; + // minimap texture handles GLuint m_TerrainTexture; @@ -95,10 +114,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(const CMatrix3D& transform, 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 @@ -224,6 +224,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 +271,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 +285,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 +326,47 @@ glDisable(GL_SCISSOR_TEST); } +// TODO: Hacky and ugly copypasta +void CMiniMap::DrawFlare(const CMatrix3D& transform, const MapFlareObj& flare, double cur_time) const +{ + float step = std::fmod((cur_time - flare.time) * 10, 10); + float viewVerts[] = { + flare.pos.x + step, -flare.pos.y-step, + flare.pos.x - step, -flare.pos.y-step, + flare.pos.x - step, -flare.pos.y+step, + flare.pos.x + step, -flare.pos.y+step + }; + const float width = m_CachedActualSize.GetWidth(); + const float height = m_CachedActualSize.GetHeight(); + // Enable Scissoring to restrict the rectangle to only the minimap. + glScissor( + m_CachedActualSize.left * g_GuiScale, + g_Renderer.GetHeight() - m_CachedActualSize.bottom * g_GuiScale, + width * g_GuiScale, + height * g_GuiScale); + glEnable(GL_SCISSOR_TEST); + glLineWidth(2.0f); + + CShaderDefines lineDefines; + lineDefines.Add(str_MINIMAP_LINE, str_1); + CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), lineDefines); + tech->BeginPass(); + CShaderProgramPtr shader = tech->GetShader(); + shader->Uniform(str_transform, transform); + shader->Uniform(str_color, flare.color.r, flare.color.g, flare.color.b, flare.color.a); + + shader->VertexPointer(2, GL_FLOAT, 0, viewVerts); + shader->AssertPointersBound(); + + if (!g_Renderer.m_SkipSubmit) + glDrawArrays(GL_LINE_LOOP, 0, 4); + + tech->EndPass(); + + glLineWidth(1.0f); + glDisable(GL_SCISSOR_TEST); +} + struct MinimapUnitVertex { // This struct is copyable for convenience and because to move is to copy for primitives. @@ -612,6 +657,13 @@ DrawViewRect(unitMatrix); + while (!m_MapFlares.empty() && 3 + m_MapFlares.front().time < cur_time) + m_MapFlares.pop_front(); + + for (const MapFlareObj& flare : m_MapFlares) + DrawFlare(unitMatrix, flare, cur_time); + + PROFILE_END("minimap units"); // Reset depth mask @@ -708,6 +760,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)