Line data Source code
1 : /* Copyright (C) 2023 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "CMiniMap.h"
21 :
22 : #include "graphics/Canvas2D.h"
23 : #include "graphics/GameView.h"
24 : #include "graphics/MiniMapTexture.h"
25 : #include "graphics/MiniPatch.h"
26 : #include "graphics/Terrain.h"
27 : #include "graphics/TerrainTextureEntry.h"
28 : #include "graphics/TerrainTextureManager.h"
29 : #include "graphics/TextureManager.h"
30 : #include "gui/CGUI.h"
31 : #include "gui/GUIManager.h"
32 : #include "lib/bits.h"
33 : #include "lib/external_libraries/libsdl.h"
34 : #include "lib/timer.h"
35 : #include "maths/MathUtil.h"
36 : #include "ps/CLogger.h"
37 : #include "ps/ConfigDB.h"
38 : #include "ps/Filesystem.h"
39 : #include "ps/Game.h"
40 : #include "ps/GameSetup/Config.h"
41 : #include "ps/Profile.h"
42 : #include "ps/World.h"
43 : #include "renderer/Renderer.h"
44 : #include "renderer/SceneRenderer.h"
45 : #include "renderer/WaterManager.h"
46 : #include "scriptinterface/Object.h"
47 : #include "simulation2/Simulation2.h"
48 : #include "simulation2/components/ICmpMinimap.h"
49 : #include "simulation2/components/ICmpRangeManager.h"
50 : #include "simulation2/helpers/Los.h"
51 : #include "simulation2/system/ParamNode.h"
52 :
53 : #include <array>
54 : #include <cmath>
55 : #include <vector>
56 :
57 : namespace
58 : {
59 :
60 : // Adds segments pieces lying inside the circle to lines.
61 0 : void CropPointsByCircle(const std::array<CVector3D, 4>& points, const CVector3D& center, const float radius, std::vector<CVector3D>* lines)
62 : {
63 0 : constexpr float EPS = 1e-3f;
64 0 : lines->reserve(points.size() * 2);
65 0 : for (size_t idx = 0; idx < points.size(); ++idx)
66 : {
67 0 : const CVector3D& currentPoint = points[idx];
68 0 : const CVector3D& nextPoint = points[(idx + 1) % points.size()];
69 0 : const CVector3D direction = (nextPoint - currentPoint).Normalized();
70 0 : const CVector3D normal(direction.Z, 0.0f, -direction.X);
71 0 : const float offset = normal.Dot(currentPoint) - normal.Dot(center);
72 : // We need to have lines only inside the circle.
73 0 : if (std::abs(offset) + EPS >= radius)
74 0 : continue;
75 0 : const CVector3D closestPoint = center + normal * offset;
76 0 : const float halfChordLength = sqrt(radius * radius - offset * offset);
77 0 : const CVector3D intersectionA = closestPoint - direction * halfChordLength;
78 0 : const CVector3D intersectionB = closestPoint + direction * halfChordLength;
79 : // We have no intersection if the segment is lying outside of the circle.
80 0 : if (direction.Dot(currentPoint) + EPS > direction.Dot(intersectionB) ||
81 0 : direction.Dot(nextPoint) - EPS < direction.Dot(intersectionA))
82 0 : continue;
83 :
84 0 : lines->emplace_back(
85 0 : direction.Dot(currentPoint) > direction.Dot(intersectionA) ? currentPoint : intersectionA);
86 0 : lines->emplace_back(
87 0 : direction.Dot(nextPoint) < direction.Dot(intersectionB) ? nextPoint : intersectionB);
88 : }
89 0 : }
90 :
91 : } // anonymous namespace
92 :
93 1 : const CStr CMiniMap::EventNameWorldClick = "WorldClick";
94 :
95 0 : CMiniMap::CMiniMap(CGUI& pGUI) :
96 : IGUIObject(pGUI),
97 : m_MapSize(0), m_MapScale(1.f), m_Mask(this, "mask", false),
98 : m_FlareTextureCount(this, "flare_texture_count", 0), m_FlareRenderSize(this, "flare_render_size", 0),
99 : m_FlareInterleave(this, "flare_interleave", false), m_FlareAnimationSpeed(this, "flare_animation_speed", 0.0f),
100 : m_FlareLifetimeSeconds(this, "flare_lifetime_seconds", 0.0f),
101 : m_FlareStartFadeSeconds(this, "flare_start_fade_seconds", 0.0f),
102 0 : m_FlareStopFadeSeconds(this, "flare_stop_fade_seconds", 0.0f)
103 : {
104 0 : m_Clicking = false;
105 0 : m_MouseHovering = false;
106 0 : }
107 :
108 : CMiniMap::~CMiniMap() = default;
109 :
110 0 : void CMiniMap::HandleMessage(SGUIMessage& Message)
111 : {
112 0 : IGUIObject::HandleMessage(Message);
113 0 : switch (Message.type)
114 : {
115 0 : case GUIM_LOAD:
116 0 : RecreateFlareTextures();
117 0 : break;
118 0 : case GUIM_SETTINGS_UPDATED:
119 0 : if (Message.value == "flare_texture_count")
120 0 : RecreateFlareTextures();
121 0 : break;
122 0 : case GUIM_MOUSE_PRESS_LEFT:
123 0 : if (m_MouseHovering)
124 : {
125 0 : if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1))
126 : {
127 0 : SetCameraPositionFromMousePosition();
128 0 : m_Clicking = true;
129 : }
130 : }
131 0 : break;
132 0 : case GUIM_MOUSE_RELEASE_LEFT:
133 0 : if (m_MouseHovering && m_Clicking)
134 0 : SetCameraPositionFromMousePosition();
135 0 : m_Clicking = false;
136 0 : break;
137 0 : case GUIM_MOUSE_DBLCLICK_LEFT:
138 0 : if (m_MouseHovering && m_Clicking)
139 0 : SetCameraPositionFromMousePosition();
140 0 : m_Clicking = false;
141 0 : break;
142 0 : case GUIM_MOUSE_ENTER:
143 0 : m_MouseHovering = true;
144 0 : break;
145 0 : case GUIM_MOUSE_LEAVE:
146 0 : m_Clicking = false;
147 0 : m_MouseHovering = false;
148 0 : break;
149 0 : case GUIM_MOUSE_RELEASE_RIGHT:
150 0 : CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1);
151 0 : break;
152 0 : case GUIM_MOUSE_DBLCLICK_RIGHT:
153 0 : CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2);
154 0 : break;
155 0 : case GUIM_MOUSE_MOTION:
156 0 : if (m_MouseHovering && m_Clicking)
157 0 : SetCameraPositionFromMousePosition();
158 0 : break;
159 0 : case GUIM_MOUSE_WHEEL_DOWN:
160 : case GUIM_MOUSE_WHEEL_UP:
161 0 : Message.Skip();
162 0 : break;
163 :
164 0 : default:
165 0 : break;
166 : }
167 0 : }
168 :
169 0 : void CMiniMap::RecreateFlareTextures()
170 : {
171 : // Catch invalid values.
172 0 : if (m_FlareTextureCount > 99)
173 : {
174 0 : LOGERROR("Invalid value for flare texture count. Valid range is 0-99.");
175 0 : return;
176 : }
177 0 : const CStr textureNumberingFormat = "art/textures/animated/minimap-flare/frame%02u.png";
178 0 : m_FlareTextures.clear();
179 0 : m_FlareTextures.reserve(m_FlareTextureCount);
180 0 : for (u32 i = 0; i < m_FlareTextureCount; ++i)
181 : {
182 0 : CTextureProperties textureProps(fmt::sprintf(textureNumberingFormat, i).c_str());
183 0 : textureProps.SetIgnoreQuality(true);
184 0 : m_FlareTextures.emplace_back(g_Renderer.GetTextureManager().CreateTexture(textureProps));
185 : }
186 : }
187 :
188 0 : bool CMiniMap::IsMouseOver() const
189 : {
190 0 : const CVector2D& mousePos = m_pGUI.GetMousePos();
191 : // Take the magnitude of the difference of the mouse position and minimap center.
192 0 : const float distanceFromCenter = (mousePos - m_CachedActualSize.CenterPoint()).Length();
193 : // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap.
194 0 : return distanceFromCenter < m_CachedActualSize.GetWidth() / 2.0;
195 : }
196 :
197 0 : void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const
198 : {
199 : // Determine X and Z according to proportion of mouse position and minimap.
200 0 : const CVector2D& mousePos = m_pGUI.GetMousePos();
201 :
202 0 : float px = (mousePos.X - m_CachedActualSize.left) / m_CachedActualSize.GetWidth();
203 0 : float py = (m_CachedActualSize.bottom - mousePos.Y) / m_CachedActualSize.GetHeight();
204 :
205 0 : float angle = GetAngle();
206 :
207 : // Scale world coordinates for shrunken square map
208 0 : x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5);
209 0 : z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5);
210 0 : }
211 :
212 0 : void CMiniMap::SetCameraPositionFromMousePosition()
213 : {
214 0 : CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
215 :
216 0 : CVector3D target;
217 0 : GetMouseWorldCoordinates(target.X, target.Z);
218 0 : target.Y = terrain->GetExactGroundLevel(target.X, target.Z);
219 0 : g_Game->GetView()->MoveCameraTarget(target);
220 0 : }
221 :
222 0 : float CMiniMap::GetAngle() const
223 : {
224 0 : CVector3D cameraIn = g_Game->GetView()->GetCamera()->GetOrientation().GetIn();
225 0 : return -atan2(cameraIn.X, cameraIn.Z);
226 : }
227 :
228 0 : CVector2D CMiniMap::WorldSpaceToMiniMapSpace(const CVector3D& worldPosition) const
229 : {
230 : // Coordinates with 0,0 in the middle of the minimap and +-0.5 as max.
231 0 : const float invTileMapSize = 1.0f / static_cast<float>(TERRAIN_TILE_SIZE * m_MapSize);
232 0 : const float relativeX = (worldPosition.X * invTileMapSize - 0.5) / m_MapScale;
233 0 : const float relativeY = (worldPosition.Z * invTileMapSize - 0.5) / m_MapScale;
234 :
235 : // Rotate coordinates.
236 0 : const float angle = GetAngle();
237 0 : const float rotatedX = cos(angle) * relativeX + sin(angle) * relativeY;
238 0 : const float rotatedY = -sin(angle) * relativeX + cos(angle) * relativeY;
239 :
240 : // Calculate coordinates in GUI space.
241 0 : return CVector2D(
242 0 : m_CachedActualSize.left + (0.5f + rotatedX) * m_CachedActualSize.GetWidth(),
243 0 : m_CachedActualSize.bottom - (0.5f + rotatedY) * m_CachedActualSize.GetHeight());
244 : }
245 :
246 0 : bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks))
247 : {
248 0 : ScriptRequest rq(g_GUI->GetActiveGUI()->GetScriptInterface());
249 :
250 : float x, z;
251 0 : GetMouseWorldCoordinates(x, z);
252 :
253 0 : JS::RootedValue coords(rq.cx);
254 0 : Script::CreateObject(rq, &coords, "x", x, "z", z);
255 :
256 0 : JS::RootedValue buttonJs(rq.cx);
257 0 : Script::ToJSVal(rq, &buttonJs, button);
258 :
259 0 : JS::RootedValueVector paramData(rq.cx);
260 0 : ignore_result(paramData.append(coords));
261 0 : ignore_result(paramData.append(buttonJs));
262 :
263 0 : return ScriptEventWithReturn(EventNameWorldClick, paramData);
264 : }
265 :
266 : // This sets up and draws the rectangle on the minimap
267 : // which represents the view of the camera in the world.
268 0 : void CMiniMap::DrawViewRect(CCanvas2D& canvas) const
269 : {
270 : // Compute the camera frustum intersected with a fixed-height plane.
271 : // Use the water height as a fixed base height, which should be the lowest we can go
272 0 : const float sampleHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight;
273 :
274 0 : const CCamera* camera = g_Game->GetView()->GetCamera();
275 : const std::array<CVector3D, 4> hitPoints = {
276 0 : camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), sampleHeight),
277 0 : camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), sampleHeight),
278 0 : camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, sampleHeight),
279 : camera->GetWorldCoordinates(0, 0, sampleHeight)
280 0 : };
281 :
282 0 : std::vector<CVector3D> worldSpaceLines;
283 : // We need to prevent drawing view bounds out of the map.
284 0 : const float halfMapSize = static_cast<float>((m_MapSize - 1) * TERRAIN_TILE_SIZE) * 0.5f;
285 0 : CropPointsByCircle(hitPoints, CVector3D(halfMapSize, 0.0f, halfMapSize), halfMapSize * m_MapScale, &worldSpaceLines);
286 0 : if (worldSpaceLines.empty())
287 0 : return;
288 :
289 0 : for (size_t index = 0; index < worldSpaceLines.size() && index + 1 < worldSpaceLines.size(); index += 2)
290 : {
291 0 : const CVector2D from = WorldSpaceToMiniMapSpace(worldSpaceLines[index]);
292 0 : const CVector2D to = WorldSpaceToMiniMapSpace(worldSpaceLines[index + 1]);
293 0 : canvas.DrawLine({from, to}, 2.0f, CColor(1.0f, 0.3f, 0.3f, 1.0f));
294 : }
295 : }
296 :
297 0 : void CMiniMap::DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double currentTime) const
298 : {
299 0 : if (m_FlareTextures.empty())
300 0 : return;
301 :
302 0 : const CVector2D flareCenter = WorldSpaceToMiniMapSpace(CVector3D(flare.pos.X, 0.0f, flare.pos.Y));
303 :
304 : const CRect destination(
305 0 : flareCenter.X - m_FlareRenderSize, flareCenter.Y - m_FlareRenderSize,
306 0 : flareCenter.X + m_FlareRenderSize, flareCenter.Y + m_FlareRenderSize);
307 :
308 0 : const double deltaTime = currentTime - flare.time;
309 0 : const double remainingTime = m_FlareLifetimeSeconds - deltaTime;
310 0 : const u32 flooredStep = floor(deltaTime * m_FlareAnimationSpeed);
311 :
312 0 : const float startFadeAlpha = m_FlareStartFadeSeconds > 0.0f ? deltaTime / m_FlareStartFadeSeconds : 1.0f;
313 0 : const float stopFadeAlpha = m_FlareStopFadeSeconds > 0.0f ? remainingTime / m_FlareStopFadeSeconds : 1.0f;
314 0 : const float alpha = Clamp(std::min(
315 0 : SmoothStep(0.0f, 1.0f, startFadeAlpha), SmoothStep(0.0f, 1.0f, stopFadeAlpha)),
316 0 : 0.0f, 1.0f);
317 :
318 0 : DrawFlareFrame(canvas, flooredStep % m_FlareTextures.size(), destination, flare.color, alpha);
319 :
320 : // Draw a second circle if the first has reached half of the animation.
321 0 : if (m_FlareInterleave && flooredStep >= m_FlareTextures.size() / 2)
322 : {
323 0 : DrawFlareFrame(canvas, (flooredStep - m_FlareTextures.size() / 2) % m_FlareTextures.size(),
324 : destination, flare.color, alpha);
325 : }
326 : }
327 :
328 0 : void CMiniMap::DrawFlareFrame(CCanvas2D& canvas, const u32 frameIndex,
329 : const CRect& destination, const CColor& color, float alpha) const
330 : {
331 : // TODO: Only draw inside the minimap circle.
332 0 : CTexturePtr texture = m_FlareTextures[frameIndex % m_FlareTextures.size()];
333 0 : CColor finalColor = color;
334 0 : finalColor.a *= alpha;
335 0 : canvas.DrawTexture(texture, destination,
336 0 : CRect(0, 0, texture->GetWidth(), texture->GetHeight()), finalColor,
337 0 : CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f);
338 0 : }
339 :
340 0 : void CMiniMap::Draw(CCanvas2D& canvas)
341 : {
342 0 : PROFILE3("render minimap");
343 :
344 : // The terrain isn't actually initialized until the map is loaded, which
345 : // happens when the game is started, so abort until then.
346 0 : if (!g_Game || !g_Game->IsGameStarted())
347 0 : return;
348 :
349 0 : if (!m_Mask)
350 0 : canvas.DrawRect(m_CachedActualSize, CColor(0.0f, 0.0f, 0.0f, 1.0f));
351 :
352 0 : CSimulation2* sim = g_Game->GetSimulation2();
353 0 : CmpPtr<ICmpRangeManager> cmpRangeManager(*sim, SYSTEM_ENTITY);
354 0 : ENSURE(cmpRangeManager);
355 :
356 : // Set our globals in case they hadn't been set before
357 0 : const CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
358 0 : m_MapSize = terrain->GetVerticesPerSide();
359 0 : m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f);
360 :
361 : // Draw the main textured quad
362 0 : CMiniMapTexture& miniMapTexture = g_Game->GetView()->GetMiniMapTexture();
363 0 : if (miniMapTexture.GetTexture())
364 : {
365 0 : const CVector2D center = m_CachedActualSize.CenterPoint();
366 : const CRect source(
367 : 0,
368 0 : miniMapTexture.IsFlipped() ? 0 : miniMapTexture.GetTexture()->GetHeight(),
369 0 : miniMapTexture.GetTexture()->GetWidth(),
370 0 : miniMapTexture.IsFlipped() ? miniMapTexture.GetTexture()->GetHeight() : 0);
371 0 : const CSize2D size(m_CachedActualSize.GetSize() / m_MapScale);
372 0 : const CRect destination(center - size / 2.0f, size);
373 0 : canvas.DrawRotatedTexture(
374 : miniMapTexture.GetTexture(), destination, source,
375 0 : CColor(1.0f, 1.0f, 1.0f, 1.0f), CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f,
376 : center, GetAngle());
377 : }
378 :
379 0 : for (const CMiniMapTexture::Icon& icon : miniMapTexture.GetIcons())
380 : {
381 : const CVector2D center = WorldSpaceToMiniMapSpace(
382 0 : CVector3D(icon.worldPosition.X, 0.0f, icon.worldPosition.Y));
383 : const CRect destination(
384 0 : center.X - icon.halfSize, center.Y - icon.halfSize,
385 0 : center.X + icon.halfSize, center.Y + icon.halfSize);
386 0 : const CRect source(0, 0, icon.texture->GetWidth(), icon.texture->GetHeight());
387 0 : canvas.DrawTexture(
388 : icon.texture, destination, source,
389 0 : icon.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f);
390 : }
391 :
392 0 : PROFILE_START("minimap flares");
393 :
394 0 : DrawViewRect(canvas);
395 :
396 0 : const double currentTime = timer_Time();
397 0 : while (!m_MapFlares.empty() && m_FlareLifetimeSeconds + m_MapFlares.front().time < currentTime)
398 0 : m_MapFlares.pop_front();
399 :
400 0 : for (const MapFlare& flare : m_MapFlares)
401 0 : DrawFlare(canvas, flare, currentTime);
402 :
403 : PROFILE_END("minimap flares");
404 : }
405 :
406 0 : bool CMiniMap::Flare(const CVector2D& pos, const CStr& colorStr)
407 : {
408 0 : CColor color;
409 0 : if (!color.ParseString(colorStr))
410 : {
411 0 : LOGERROR("CMiniMap::Flare: Couldn't parse color string");
412 0 : return false;
413 : }
414 0 : m_MapFlares.push_back({ pos, color, timer_Time() });
415 0 : return true;
416 3 : }
|