mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Merge pull request #61 from ldmonster/feat/map-system
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
[chore] refactor: Decompose World Map into Modular Component Architecture
This commit is contained in:
commit
09c4a9a04a
55 changed files with 6335 additions and 1542 deletions
|
|
@ -634,7 +634,25 @@ set(WOWEE_SOURCES
|
||||||
src/rendering/hiz_system.cpp
|
src/rendering/hiz_system.cpp
|
||||||
src/rendering/quest_marker_renderer.cpp
|
src/rendering/quest_marker_renderer.cpp
|
||||||
src/rendering/minimap.cpp
|
src/rendering/minimap.cpp
|
||||||
src/rendering/world_map.cpp
|
src/rendering/world_map/coordinate_projection.cpp
|
||||||
|
src/rendering/world_map/map_resolver.cpp
|
||||||
|
src/rendering/world_map/exploration_state.cpp
|
||||||
|
src/rendering/world_map/zone_metadata.cpp
|
||||||
|
src/rendering/world_map/data_repository.cpp
|
||||||
|
src/rendering/world_map/view_state_machine.cpp
|
||||||
|
src/rendering/world_map/composite_renderer.cpp
|
||||||
|
src/rendering/world_map/overlay_renderer.cpp
|
||||||
|
src/rendering/world_map/input_handler.cpp
|
||||||
|
src/rendering/world_map/world_map_facade.cpp
|
||||||
|
src/rendering/world_map/layers/player_marker_layer.cpp
|
||||||
|
src/rendering/world_map/layers/party_dot_layer.cpp
|
||||||
|
src/rendering/world_map/layers/taxi_node_layer.cpp
|
||||||
|
src/rendering/world_map/layers/poi_marker_layer.cpp
|
||||||
|
src/rendering/world_map/layers/quest_poi_layer.cpp
|
||||||
|
src/rendering/world_map/layers/corpse_marker_layer.cpp
|
||||||
|
src/rendering/world_map/layers/zone_highlight_layer.cpp
|
||||||
|
src/rendering/world_map/layers/coordinate_display.cpp
|
||||||
|
src/rendering/world_map/layers/subzone_tooltip_layer.cpp
|
||||||
src/rendering/swim_effects.cpp
|
src/rendering/swim_effects.cpp
|
||||||
src/rendering/mount_dust.cpp
|
src/rendering/mount_dust.cpp
|
||||||
src/rendering/levelup_effect.cpp
|
src/rendering/levelup_effect.cpp
|
||||||
|
|
@ -778,6 +796,26 @@ set(WOWEE_HEADERS
|
||||||
include/rendering/lightning.hpp
|
include/rendering/lightning.hpp
|
||||||
include/rendering/swim_effects.hpp
|
include/rendering/swim_effects.hpp
|
||||||
include/rendering/world_map.hpp
|
include/rendering/world_map.hpp
|
||||||
|
include/rendering/world_map/world_map_types.hpp
|
||||||
|
include/rendering/world_map/coordinate_projection.hpp
|
||||||
|
include/rendering/world_map/map_resolver.hpp
|
||||||
|
include/rendering/world_map/exploration_state.hpp
|
||||||
|
include/rendering/world_map/zone_metadata.hpp
|
||||||
|
include/rendering/world_map/data_repository.hpp
|
||||||
|
include/rendering/world_map/view_state_machine.hpp
|
||||||
|
include/rendering/world_map/composite_renderer.hpp
|
||||||
|
include/rendering/world_map/overlay_renderer.hpp
|
||||||
|
include/rendering/world_map/input_handler.hpp
|
||||||
|
include/rendering/world_map/world_map_facade.hpp
|
||||||
|
include/rendering/world_map/layers/player_marker_layer.hpp
|
||||||
|
include/rendering/world_map/layers/party_dot_layer.hpp
|
||||||
|
include/rendering/world_map/layers/taxi_node_layer.hpp
|
||||||
|
include/rendering/world_map/layers/poi_marker_layer.hpp
|
||||||
|
include/rendering/world_map/layers/quest_poi_layer.hpp
|
||||||
|
include/rendering/world_map/layers/corpse_marker_layer.hpp
|
||||||
|
include/rendering/world_map/layers/zone_highlight_layer.hpp
|
||||||
|
include/rendering/world_map/layers/coordinate_display.hpp
|
||||||
|
include/rendering/world_map/layers/subzone_tooltip_layer.hpp
|
||||||
include/rendering/character_renderer.hpp
|
include/rendering/character_renderer.hpp
|
||||||
include/rendering/character_preview.hpp
|
include/rendering/character_preview.hpp
|
||||||
include/rendering/wmo_renderer.hpp
|
include/rendering/wmo_renderer.hpp
|
||||||
|
|
|
||||||
16
assets/shaders/world_map_fog.frag.glsl
Normal file
16
assets/shaders/world_map_fog.frag.glsl
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform sampler2D uTileTexture;
|
||||||
|
|
||||||
|
layout(push_constant) uniform PushConstants {
|
||||||
|
layout(offset = 16) vec4 tintColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 TexCoord;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 texel = texture(uTileTexture, TexCoord);
|
||||||
|
outColor = texel * tintColor;
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,7 @@ public:
|
||||||
|
|
||||||
// Map name utilities
|
// Map name utilities
|
||||||
static const char* mapIdToName(uint32_t mapId);
|
static const char* mapIdToName(uint32_t mapId);
|
||||||
|
static int mapNameToId(const std::string& name);
|
||||||
static const char* mapDisplayName(uint32_t mapId);
|
static const char* mapDisplayName(uint32_t mapId);
|
||||||
|
|
||||||
// Background preloading — warms AssetManager file cache
|
// Background preloading — warms AssetManager file cache
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ class CharacterRenderer;
|
||||||
class WMORenderer;
|
class WMORenderer;
|
||||||
class M2Renderer;
|
class M2Renderer;
|
||||||
class Minimap;
|
class Minimap;
|
||||||
class WorldMap;
|
namespace world_map { class WorldMapFacade; }
|
||||||
|
using WorldMap = world_map::WorldMapFacade;
|
||||||
class QuestMarkerRenderer;
|
class QuestMarkerRenderer;
|
||||||
class CharacterPreview;
|
class CharacterPreview;
|
||||||
class AmdFsr3Runtime;
|
class AmdFsr3Runtime;
|
||||||
|
|
|
||||||
|
|
@ -1,174 +1,23 @@
|
||||||
|
// world_map.hpp — Shim header for backward compatibility.
|
||||||
|
// Redirects to the modular world_map/world_map_facade.hpp.
|
||||||
|
// Consumers should migrate to #include "rendering/world_map/world_map_facade.hpp" directly.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <vulkan/vulkan.h>
|
#include "rendering/world_map/world_map_facade.hpp"
|
||||||
#include <vk_mem_alloc.h>
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace pipeline { class AssetManager; }
|
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
class VkContext;
|
// Backward-compatible type aliases for old consumer code
|
||||||
class VkTexture;
|
// (game_screen_hud.cpp, renderer.cpp, etc.)
|
||||||
class VkRenderTarget;
|
using WorldMapPartyDot = world_map::PartyDot;
|
||||||
|
using WorldMapTaxiNode = world_map::TaxiNode;
|
||||||
|
using MapPOI = world_map::POI;
|
||||||
|
|
||||||
/// Party member dot passed in from the UI layer for world map overlay.
|
// WorldMap alias is already provided by world_map_facade.hpp:
|
||||||
struct WorldMapPartyDot {
|
// using WorldMap = world_map::WorldMapFacade;
|
||||||
glm::vec3 renderPos; ///< Position in render-space coordinates
|
// WorldMap::QuestPoi alias is provided inside WorldMapFacade:
|
||||||
uint32_t color; ///< RGBA packed color (IM_COL32 format)
|
// using QuestPoi = QuestPOI;
|
||||||
std::string name; ///< Member name (shown as tooltip on hover)
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Taxi (flight master) node passed from the UI layer for world map overlay.
|
|
||||||
struct WorldMapTaxiNode {
|
|
||||||
uint32_t id = 0; ///< TaxiNodes.dbc ID
|
|
||||||
uint32_t mapId = 0; ///< WoW internal map ID (0=EK,1=Kal,530=Outland,571=Northrend)
|
|
||||||
float wowX = 0, wowY = 0, wowZ = 0; ///< Canonical WoW coordinates
|
|
||||||
std::string name; ///< Node name (shown as tooltip)
|
|
||||||
bool known = false; ///< Player has discovered this node
|
|
||||||
};
|
|
||||||
|
|
||||||
struct WorldMapZone {
|
|
||||||
uint32_t wmaID = 0;
|
|
||||||
uint32_t areaID = 0; // 0 = continent level
|
|
||||||
std::string areaName; // texture folder name (from DBC)
|
|
||||||
float locLeft = 0, locRight = 0, locTop = 0, locBottom = 0;
|
|
||||||
uint32_t displayMapID = 0;
|
|
||||||
uint32_t parentWorldMapID = 0;
|
|
||||||
std::vector<uint32_t> exploreBits; // all AreaBit indices (zone + subzones)
|
|
||||||
|
|
||||||
// Per-zone cached textures (owned by WorldMap::zoneTextures)
|
|
||||||
VkTexture* tileTextures[12] = {};
|
|
||||||
bool tilesLoaded = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class WorldMap {
|
|
||||||
public:
|
|
||||||
WorldMap();
|
|
||||||
~WorldMap();
|
|
||||||
|
|
||||||
bool initialize(VkContext* ctx, pipeline::AssetManager* assetManager);
|
|
||||||
void shutdown();
|
|
||||||
|
|
||||||
/// Off-screen composite pass — call BEFORE the main render pass begins.
|
|
||||||
void compositePass(VkCommandBuffer cmd);
|
|
||||||
|
|
||||||
/// ImGui overlay — call INSIDE the main render pass (during ImGui frame).
|
|
||||||
void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight,
|
|
||||||
float playerYawDeg = 0.0f);
|
|
||||||
|
|
||||||
void setMapName(const std::string& name);
|
|
||||||
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
|
||||||
void setPartyDots(std::vector<WorldMapPartyDot> dots) { partyDots_ = std::move(dots); }
|
|
||||||
void setTaxiNodes(std::vector<WorldMapTaxiNode> nodes) { taxiNodes_ = std::move(nodes); }
|
|
||||||
|
|
||||||
/// Quest POI marker for world map overlay (from SMSG_QUEST_POI_QUERY_RESPONSE).
|
|
||||||
struct QuestPoi {
|
|
||||||
float wowX = 0, wowY = 0; ///< Canonical WoW coordinates (centroid of POI area)
|
|
||||||
std::string name; ///< Quest title
|
|
||||||
};
|
|
||||||
void setQuestPois(std::vector<QuestPoi> pois) { questPois_ = std::move(pois); }
|
|
||||||
/// Set the player's corpse position for overlay rendering.
|
|
||||||
/// @param hasCorpse True when the player is a ghost with an unclaimed corpse on this map.
|
|
||||||
/// @param renderPos Corpse position in render-space coordinates.
|
|
||||||
void setCorpsePos(bool hasCorpse, glm::vec3 renderPos) {
|
|
||||||
hasCorpse_ = hasCorpse;
|
|
||||||
corpseRenderPos_ = renderPos;
|
|
||||||
}
|
|
||||||
bool isOpen() const { return open; }
|
|
||||||
void close() { open = false; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum class ViewLevel { WORLD, CONTINENT, ZONE };
|
|
||||||
|
|
||||||
void enterWorldView();
|
|
||||||
void loadZonesFromDBC();
|
|
||||||
int findBestContinentForPlayer(const glm::vec3& playerRenderPos) const;
|
|
||||||
int findZoneForPlayer(const glm::vec3& playerRenderPos) const;
|
|
||||||
bool zoneBelongsToContinent(int zoneIdx, int contIdx) const;
|
|
||||||
bool getContinentProjectionBounds(int contIdx, float& left, float& right,
|
|
||||||
float& top, float& bottom) const;
|
|
||||||
void loadZoneTextures(int zoneIdx);
|
|
||||||
void requestComposite(int zoneIdx);
|
|
||||||
void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight,
|
|
||||||
float playerYawDeg);
|
|
||||||
void updateExploration(const glm::vec3& playerRenderPos);
|
|
||||||
void zoomIn(const glm::vec3& playerRenderPos);
|
|
||||||
void zoomOut();
|
|
||||||
glm::vec2 renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) const;
|
|
||||||
void destroyZoneTextures();
|
|
||||||
|
|
||||||
VkContext* vkCtx = nullptr;
|
|
||||||
pipeline::AssetManager* assetManager = nullptr;
|
|
||||||
bool initialized = false;
|
|
||||||
bool open = false;
|
|
||||||
|
|
||||||
std::string mapName = "Azeroth";
|
|
||||||
|
|
||||||
// All zones for current map
|
|
||||||
std::vector<WorldMapZone> zones;
|
|
||||||
int continentIdx = -1;
|
|
||||||
int currentIdx = -1;
|
|
||||||
ViewLevel viewLevel = ViewLevel::CONTINENT;
|
|
||||||
int compositedIdx = -1;
|
|
||||||
int pendingCompositeIdx = -1;
|
|
||||||
|
|
||||||
// FBO replacement (4x3 tiles = 1024x768)
|
|
||||||
static constexpr int GRID_COLS = 4;
|
|
||||||
static constexpr int GRID_ROWS = 3;
|
|
||||||
static constexpr int TILE_PX = 256;
|
|
||||||
static constexpr int FBO_W = GRID_COLS * TILE_PX;
|
|
||||||
static constexpr int FBO_H = GRID_ROWS * TILE_PX;
|
|
||||||
|
|
||||||
std::unique_ptr<VkRenderTarget> compositeTarget;
|
|
||||||
|
|
||||||
// Quad vertex buffer (pos2 + uv2)
|
|
||||||
::VkBuffer quadVB = VK_NULL_HANDLE;
|
|
||||||
VmaAllocation quadVBAlloc = VK_NULL_HANDLE;
|
|
||||||
|
|
||||||
// Descriptor resources
|
|
||||||
VkDescriptorSetLayout samplerSetLayout = VK_NULL_HANDLE;
|
|
||||||
VkDescriptorPool descPool = VK_NULL_HANDLE;
|
|
||||||
static constexpr uint32_t MAX_DESC_SETS = 32;
|
|
||||||
|
|
||||||
// Tile composite pipeline
|
|
||||||
VkPipeline tilePipeline = VK_NULL_HANDLE;
|
|
||||||
VkPipelineLayout tilePipelineLayout = VK_NULL_HANDLE;
|
|
||||||
VkDescriptorSet tileDescSets[2][12] = {}; // [frameInFlight][tileSlot]
|
|
||||||
|
|
||||||
// ImGui display descriptor set (points to composite render target)
|
|
||||||
VkDescriptorSet imguiDisplaySet = VK_NULL_HANDLE;
|
|
||||||
|
|
||||||
// Texture storage (owns all VkTexture objects for zone tiles)
|
|
||||||
std::vector<std::unique_ptr<VkTexture>> zoneTextures;
|
|
||||||
|
|
||||||
// Party member dots (set each frame from the UI layer)
|
|
||||||
std::vector<WorldMapPartyDot> partyDots_;
|
|
||||||
|
|
||||||
// Taxi node markers (set each frame from the UI layer)
|
|
||||||
std::vector<WorldMapTaxiNode> taxiNodes_;
|
|
||||||
int currentMapId_ = -1; ///< WoW map ID currently loaded (set in loadZonesFromDBC)
|
|
||||||
|
|
||||||
// Quest POI markers (set each frame from the UI layer)
|
|
||||||
std::vector<QuestPoi> questPois_;
|
|
||||||
|
|
||||||
// Corpse marker (ghost state — set each frame from the UI layer)
|
|
||||||
bool hasCorpse_ = false;
|
|
||||||
glm::vec3 corpseRenderPos_ = {};
|
|
||||||
|
|
||||||
// Exploration / fog of war
|
|
||||||
std::vector<uint32_t> serverExplorationMask;
|
|
||||||
bool hasServerExplorationMask = false;
|
|
||||||
std::unordered_set<int> exploredZones;
|
|
||||||
// Locally accumulated exploration (used as fallback when server mask is unavailable)
|
|
||||||
std::unordered_set<int> locallyExploredZones_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
157
include/rendering/world_map/composite_renderer.hpp
Normal file
157
include/rendering/world_map/composite_renderer.hpp
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
// composite_renderer.hpp — Vulkan off-screen composite rendering for the world map.
|
||||||
|
// Extracted from WorldMap (Phase 7 of refactoring plan).
|
||||||
|
// SRP — all GPU resource management separated from domain logic.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
#include <vk_mem_alloc.h>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
class VkContext;
|
||||||
|
class VkTexture;
|
||||||
|
class VkRenderTarget;
|
||||||
|
}
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
/// Push constant for world map tile composite vertex shader.
|
||||||
|
struct WorldMapTilePush {
|
||||||
|
glm::vec2 gridOffset; // 8 bytes
|
||||||
|
float gridCols; // 4 bytes
|
||||||
|
float gridRows; // 4 bytes
|
||||||
|
}; // 16 bytes
|
||||||
|
|
||||||
|
/// Push constant for the overlay/fog pipeline (vertex + fragment stages).
|
||||||
|
struct OverlayPush {
|
||||||
|
glm::vec2 gridOffset; // 8 bytes (vertex)
|
||||||
|
float gridCols; // 4 bytes (vertex)
|
||||||
|
float gridRows; // 4 bytes (vertex)
|
||||||
|
glm::vec4 tintColor; // 16 bytes (fragment)
|
||||||
|
}; // 32 bytes
|
||||||
|
|
||||||
|
class CompositeRenderer {
|
||||||
|
public:
|
||||||
|
CompositeRenderer();
|
||||||
|
~CompositeRenderer();
|
||||||
|
|
||||||
|
bool initialize(VkContext* ctx, pipeline::AssetManager* am);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
/// Load base tile textures for a zone.
|
||||||
|
void loadZoneTextures(int zoneIdx, std::vector<Zone>& zones, const std::string& mapName);
|
||||||
|
|
||||||
|
/// Load exploration overlay textures for a zone.
|
||||||
|
void loadOverlayTextures(int zoneIdx, std::vector<Zone>& zones);
|
||||||
|
|
||||||
|
/// Request a composite for the given zone (deferred to compositePass).
|
||||||
|
void requestComposite(int zoneIdx);
|
||||||
|
|
||||||
|
/// Execute the off-screen composite pass.
|
||||||
|
void compositePass(VkCommandBuffer cmd,
|
||||||
|
const std::vector<Zone>& zones,
|
||||||
|
const std::unordered_set<int>& exploredOverlays,
|
||||||
|
bool hasServerMask);
|
||||||
|
|
||||||
|
/// Descriptor set for ImGui display of the composite.
|
||||||
|
VkDescriptorSet displayDescriptorSet() const { return imguiDisplaySet; }
|
||||||
|
|
||||||
|
/// Destroy all loaded zone textures (on map change).
|
||||||
|
void destroyZoneTextures(std::vector<Zone>& zones);
|
||||||
|
|
||||||
|
/// Detach zone textures for deferred GPU destruction.
|
||||||
|
/// Clears CPU tracking immediately but moves GPU texture objects to a stale
|
||||||
|
/// list so they can be freed later when no in-flight frames reference them.
|
||||||
|
void detachZoneTextures();
|
||||||
|
|
||||||
|
/// Free any GPU textures previously moved to the stale list by detachZoneTextures.
|
||||||
|
/// Calls vkDeviceWaitIdle internally to ensure no in-flight work references them.
|
||||||
|
void flushStaleTextures();
|
||||||
|
|
||||||
|
/// Index of the zone currently composited (-1 if none).
|
||||||
|
int compositedIdx() const { return compositedIdx_; }
|
||||||
|
|
||||||
|
/// Reset composited index to force re-composite.
|
||||||
|
void invalidateComposite() { compositedIdx_ = -1; }
|
||||||
|
|
||||||
|
/// Check whether a zone has any loaded tile textures.
|
||||||
|
bool hasAnyTile(int zoneIdx) const;
|
||||||
|
|
||||||
|
// FBO dimensions (public for overlay coordinate math)
|
||||||
|
static constexpr int GRID_COLS = 4;
|
||||||
|
static constexpr int GRID_ROWS = 3;
|
||||||
|
static constexpr int TILE_PX = 256;
|
||||||
|
static constexpr int FBO_W = GRID_COLS * TILE_PX;
|
||||||
|
static constexpr int FBO_H = GRID_ROWS * TILE_PX;
|
||||||
|
|
||||||
|
// WoW's WorldMapDetailFrame is 1002x668 — the visible map content area.
|
||||||
|
// The FBO is 1024x768 so we crop UVs to show only the actual map region.
|
||||||
|
static constexpr int MAP_W = 1002;
|
||||||
|
static constexpr int MAP_H = 668;
|
||||||
|
static constexpr float MAP_U_MAX = static_cast<float>(MAP_W) / static_cast<float>(FBO_W);
|
||||||
|
static constexpr float MAP_V_MAX = static_cast<float>(MAP_H) / static_cast<float>(FBO_H);
|
||||||
|
|
||||||
|
private:
|
||||||
|
VkContext* vkCtx = nullptr;
|
||||||
|
pipeline::AssetManager* assetManager = nullptr;
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
|
std::unique_ptr<VkRenderTarget> compositeTarget;
|
||||||
|
|
||||||
|
// Quad vertex buffer (pos2 + uv2)
|
||||||
|
::VkBuffer quadVB = VK_NULL_HANDLE;
|
||||||
|
VmaAllocation quadVBAlloc = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Descriptor resources
|
||||||
|
VkDescriptorSetLayout samplerSetLayout = VK_NULL_HANDLE;
|
||||||
|
VkDescriptorPool descPool = VK_NULL_HANDLE;
|
||||||
|
static constexpr uint32_t MAX_DESC_SETS = 192;
|
||||||
|
static constexpr uint32_t MAX_OVERLAY_TILES = 48;
|
||||||
|
|
||||||
|
// Tile composite pipeline
|
||||||
|
VkPipeline tilePipeline = VK_NULL_HANDLE;
|
||||||
|
VkPipelineLayout tilePipelineLayout = VK_NULL_HANDLE;
|
||||||
|
VkDescriptorSet tileDescSets[2][12] = {}; // [frameInFlight][tileSlot]
|
||||||
|
|
||||||
|
// Alpha-blended overlay pipeline (fog + explored area overlays)
|
||||||
|
VkPipeline overlayPipeline_ = VK_NULL_HANDLE;
|
||||||
|
VkPipelineLayout overlayPipelineLayout_ = VK_NULL_HANDLE;
|
||||||
|
std::unique_ptr<VkTexture> fogTexture_; // 1×1 white pixel for fog quad
|
||||||
|
VkDescriptorSet fogDescSet_ = VK_NULL_HANDLE;
|
||||||
|
VkDescriptorSet overlayDescSets_[2][MAX_OVERLAY_TILES] = {};
|
||||||
|
|
||||||
|
// ImGui display descriptor set (points to composite render target)
|
||||||
|
VkDescriptorSet imguiDisplaySet = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Texture storage (owns all VkTexture objects for zone tiles)
|
||||||
|
std::vector<std::unique_ptr<VkTexture>> zoneTextures;
|
||||||
|
|
||||||
|
int compositedIdx_ = -1;
|
||||||
|
int pendingCompositeIdx_ = -1;
|
||||||
|
|
||||||
|
// Per-zone tile texture pointers (indexed by zone, then by tile slot)
|
||||||
|
// Stored separately since Zone struct is now Vulkan-free
|
||||||
|
struct ZoneTextureSlots {
|
||||||
|
VkTexture* tileTextures[12] = {};
|
||||||
|
bool tilesLoaded = false;
|
||||||
|
// Per-overlay tile textures
|
||||||
|
struct OverlaySlots {
|
||||||
|
std::vector<VkTexture*> tiles;
|
||||||
|
bool tilesLoaded = false;
|
||||||
|
};
|
||||||
|
std::vector<OverlaySlots> overlays;
|
||||||
|
};
|
||||||
|
std::vector<ZoneTextureSlots> zoneTextureSlots_;
|
||||||
|
|
||||||
|
void ensureTextureSlots(size_t zoneCount, const std::vector<Zone>& zones);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
49
include/rendering/world_map/coordinate_projection.hpp
Normal file
49
include/rendering/world_map/coordinate_projection.hpp
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// coordinate_projection.hpp — Pure coordinate math for world map UV projection.
|
||||||
|
// Extracted from WorldMap methods (Phase 2 of refactoring plan).
|
||||||
|
// All functions are stateless free functions — trivially testable.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
/// Project render-space position to [0,1] UV on a zone or continent map.
|
||||||
|
glm::vec2 renderPosToMapUV(const glm::vec3& renderPos,
|
||||||
|
const ZoneBounds& bounds,
|
||||||
|
bool isContinent);
|
||||||
|
|
||||||
|
/// Derive effective projection bounds for a continent from its child zones.
|
||||||
|
/// Uses zoneBelongsToContinent() internally. Returns false if insufficient data.
|
||||||
|
bool getContinentProjectionBounds(const std::vector<Zone>& zones,
|
||||||
|
int contIdx,
|
||||||
|
float& left, float& right,
|
||||||
|
float& top, float& bottom);
|
||||||
|
|
||||||
|
/// Find the best-fit continent index for a player position.
|
||||||
|
/// Prefers the smallest containing continent; falls back to nearest center.
|
||||||
|
int findBestContinentForPlayer(const std::vector<Zone>& zones,
|
||||||
|
const glm::vec3& playerRenderPos);
|
||||||
|
|
||||||
|
/// Find the smallest zone (areaID != 0) containing the player position.
|
||||||
|
/// Returns -1 if no zone contains the position.
|
||||||
|
int findZoneForPlayer(const std::vector<Zone>& zones,
|
||||||
|
const glm::vec3& playerRenderPos);
|
||||||
|
|
||||||
|
/// Test if a zone spatially belongs to a given continent.
|
||||||
|
/// Uses parentWorldMapID when available, falls back to overlap heuristic.
|
||||||
|
bool zoneBelongsToContinent(const std::vector<Zone>& zones,
|
||||||
|
int zoneIdx, int contIdx);
|
||||||
|
|
||||||
|
/// Check whether the zone at idx is a root continent (has leaf continents as children).
|
||||||
|
bool isRootContinent(const std::vector<Zone>& zones, int idx);
|
||||||
|
|
||||||
|
/// Check whether the zone at idx is a leaf continent (parentWorldMapID != 0, areaID == 0).
|
||||||
|
bool isLeafContinent(const std::vector<Zone>& zones, int idx);
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
95
include/rendering/world_map/data_repository.hpp
Normal file
95
include/rendering/world_map/data_repository.hpp
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
// data_repository.hpp — DBC data loading, ZMP pixel map, and zone/POI/overlay storage.
|
||||||
|
// Extracted from WorldMap::loadZonesFromDBC, loadPOIData, buildCosmicView
|
||||||
|
// (Phase 5 of refactoring plan). SRP — all DBC parsing lives here.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class DataRepository {
|
||||||
|
public:
|
||||||
|
/// Load all zone data from DBC files for the given map name.
|
||||||
|
void loadZones(const std::string& mapName, pipeline::AssetManager& assetManager);
|
||||||
|
|
||||||
|
/// Load area POI markers from AreaPOI.dbc.
|
||||||
|
void loadPOIs(pipeline::AssetManager& assetManager);
|
||||||
|
|
||||||
|
/// Build cosmic view entries for the active expansion (uses isActiveExpansion).
|
||||||
|
void buildCosmicView(int expansionLevel = 0);
|
||||||
|
|
||||||
|
/// Build Azeroth world-view continent regions for the active expansion.
|
||||||
|
void buildAzerothView(int expansionLevel = 0);
|
||||||
|
|
||||||
|
/// Load ZMP pixel map for the given continent name (e.g. "Azeroth").
|
||||||
|
/// The ZMP is a 128x128 grid of uint32 AreaTable IDs.
|
||||||
|
void loadZmpPixelMap(const std::string& continentName,
|
||||||
|
pipeline::AssetManager& assetManager);
|
||||||
|
|
||||||
|
/// Determine expansion level from the active expansion profile.
|
||||||
|
static int getExpansionLevel();
|
||||||
|
|
||||||
|
// --- Accessors ---
|
||||||
|
std::vector<Zone>& zones() { return zones_; }
|
||||||
|
const std::vector<Zone>& zones() const { return zones_; }
|
||||||
|
int cosmicIdx() const { return cosmicIdx_; }
|
||||||
|
int worldIdx() const { return worldIdx_; }
|
||||||
|
int currentMapId() const { return currentMapId_; }
|
||||||
|
const std::vector<CosmicMapEntry>& cosmicMaps() const { return cosmicMaps_; }
|
||||||
|
const std::vector<CosmicMapEntry>& azerothRegions() const { return azerothRegions_; }
|
||||||
|
bool cosmicEnabled() const { return cosmicEnabled_; }
|
||||||
|
const std::vector<POI>& poiMarkers() const { return poiMarkers_; }
|
||||||
|
|
||||||
|
const std::unordered_map<uint32_t, uint32_t>& exploreFlagByAreaId() const { return exploreFlagByAreaId_; }
|
||||||
|
const std::unordered_map<uint32_t, std::string>& areaNameByAreaId() const { return areaNameByAreaId_; }
|
||||||
|
|
||||||
|
/// ZMP pixel map accessors.
|
||||||
|
static constexpr int ZMP_SIZE = 128;
|
||||||
|
const std::array<uint32_t, 128 * 128>& zmpGrid() const { return zmpGrid_; }
|
||||||
|
bool hasZmpData() const { return zmpLoaded_; }
|
||||||
|
|
||||||
|
/// Look up zone index from an AreaTable ID (from ZMP). Returns -1 if not found.
|
||||||
|
int zoneIndexForAreaId(uint32_t areaId) const;
|
||||||
|
|
||||||
|
/// ZMP-derived bounding rectangles per zone index (UV [0,1] on display).
|
||||||
|
const std::unordered_map<int, ZmpRect>& zmpZoneBounds() const { return zmpZoneBounds_; }
|
||||||
|
|
||||||
|
/// Reset all data (called on map change).
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Zone> zones_;
|
||||||
|
std::vector<POI> poiMarkers_;
|
||||||
|
std::vector<CosmicMapEntry> cosmicMaps_;
|
||||||
|
std::vector<CosmicMapEntry> azerothRegions_;
|
||||||
|
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId_;
|
||||||
|
std::unordered_map<uint32_t, std::string> areaNameByAreaId_;
|
||||||
|
int cosmicIdx_ = -1;
|
||||||
|
int worldIdx_ = -1;
|
||||||
|
int currentMapId_ = -1;
|
||||||
|
bool cosmicEnabled_ = true;
|
||||||
|
bool poisLoaded_ = false;
|
||||||
|
|
||||||
|
// ZMP pixel map: 128x128 grid of AreaTable IDs for continent-level hover
|
||||||
|
std::array<uint32_t, 128 * 128> zmpGrid_{};
|
||||||
|
bool zmpLoaded_ = false;
|
||||||
|
// AreaID → zone index (zones_ vector) for quick resolution
|
||||||
|
std::unordered_map<uint32_t, int> areaIdToZoneIdx_;
|
||||||
|
// ZMP-derived bounding boxes per zone index (UV coords on display)
|
||||||
|
std::unordered_map<int, ZmpRect> zmpZoneBounds_;
|
||||||
|
|
||||||
|
/// Scan ZMP grid and build bounding boxes for each zone.
|
||||||
|
void buildZmpZoneBounds();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
53
include/rendering/world_map/exploration_state.hpp
Normal file
53
include/rendering/world_map/exploration_state.hpp
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// exploration_state.hpp — Fog of war / exploration tracking (pure domain logic).
|
||||||
|
// Extracted from WorldMap::updateExploration (Phase 3 of refactoring plan).
|
||||||
|
// No rendering or GPU dependencies — fully testable standalone.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class ExplorationState {
|
||||||
|
public:
|
||||||
|
void setServerMask(const std::vector<uint32_t>& masks, bool hasData);
|
||||||
|
bool hasServerMask() const { return hasServerMask_; }
|
||||||
|
|
||||||
|
/// Recompute explored zones and overlays for given player position.
|
||||||
|
/// @param zones All loaded zones
|
||||||
|
/// @param playerRenderPos Player position in render-space
|
||||||
|
/// @param currentZoneIdx Currently viewed zone index
|
||||||
|
/// @param exploreFlagByAreaId AreaID → ExploreFlag mapping from AreaTable.dbc
|
||||||
|
void update(const std::vector<Zone>& zones,
|
||||||
|
const glm::vec3& playerRenderPos,
|
||||||
|
int currentZoneIdx,
|
||||||
|
const std::unordered_map<uint32_t, uint32_t>& exploreFlagByAreaId);
|
||||||
|
|
||||||
|
const std::unordered_set<int>& exploredZones() const { return exploredZones_; }
|
||||||
|
const std::unordered_set<int>& exploredOverlays() const { return exploredOverlays_; }
|
||||||
|
|
||||||
|
/// Returns true if the explored overlay set changed since last update.
|
||||||
|
bool overlaysChanged() const { return overlaysChanged_; }
|
||||||
|
|
||||||
|
/// Clear accumulated local exploration data.
|
||||||
|
void clearLocal() { locallyExploredZones_.clear(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool isBitSet(uint32_t bitIndex) const;
|
||||||
|
|
||||||
|
std::vector<uint32_t> serverMask_;
|
||||||
|
bool hasServerMask_ = false;
|
||||||
|
std::unordered_set<int> exploredZones_;
|
||||||
|
std::unordered_set<int> exploredOverlays_;
|
||||||
|
std::unordered_set<int> locallyExploredZones_;
|
||||||
|
bool overlaysChanged_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
37
include/rendering/world_map/input_handler.hpp
Normal file
37
include/rendering/world_map/input_handler.hpp
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// input_handler.hpp — Input processing for the world map.
|
||||||
|
// Extracted from WorldMap::render (Phase 9 of refactoring plan).
|
||||||
|
// SRP — input interpretation separated from state changes and rendering.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
enum class InputAction {
|
||||||
|
NONE,
|
||||||
|
CLOSE,
|
||||||
|
ZOOM_IN,
|
||||||
|
ZOOM_OUT,
|
||||||
|
CLICK_ZONE, // left-click on continent view zone
|
||||||
|
CLICK_COSMIC_REGION, // left-click on cosmic landmass
|
||||||
|
RIGHT_CLICK_BACK, // right-click to go back
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputResult {
|
||||||
|
InputAction action = InputAction::NONE;
|
||||||
|
int targetIdx = -1; // zone or cosmic region index
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputHandler {
|
||||||
|
public:
|
||||||
|
/// Process input for current frame. Returns the highest-priority action.
|
||||||
|
InputResult process(ViewLevel currentLevel,
|
||||||
|
int hoveredZoneIdx,
|
||||||
|
bool cosmicEnabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
16
include/rendering/world_map/layers/coordinate_display.hpp
Normal file
16
include/rendering/world_map/layers/coordinate_display.hpp
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// coordinate_display.hpp — WoW coordinates under cursor on the world map.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class CoordinateDisplay : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
24
include/rendering/world_map/layers/corpse_marker_layer.hpp
Normal file
24
include/rendering/world_map/layers/corpse_marker_layer.hpp
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// corpse_marker_layer.hpp — Death corpse X marker on the world map.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class CorpseMarkerLayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void setCorpse(bool hasCorpse, glm::vec3 renderPos) {
|
||||||
|
hasCorpse_ = hasCorpse;
|
||||||
|
corpseRenderPos_ = renderPos;
|
||||||
|
}
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
private:
|
||||||
|
bool hasCorpse_ = false;
|
||||||
|
glm::vec3 corpseRenderPos_ = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
21
include/rendering/world_map/layers/party_dot_layer.hpp
Normal file
21
include/rendering/world_map/layers/party_dot_layer.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// party_dot_layer.hpp — Party member position dots on the world map.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class PartyDotLayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void setDots(const std::vector<PartyDot>& dots) { dots_ = &dots; }
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
private:
|
||||||
|
const std::vector<PartyDot>* dots_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
16
include/rendering/world_map/layers/player_marker_layer.hpp
Normal file
16
include/rendering/world_map/layers/player_marker_layer.hpp
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// player_marker_layer.hpp — Directional player arrow on the world map.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class PlayerMarkerLayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
21
include/rendering/world_map/layers/poi_marker_layer.hpp
Normal file
21
include/rendering/world_map/layers/poi_marker_layer.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// poi_marker_layer.hpp — Town/dungeon/capital POI icons on the world map.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class POIMarkerLayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void setMarkers(const std::vector<POI>& markers) { markers_ = &markers; }
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
private:
|
||||||
|
const std::vector<POI>* markers_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
21
include/rendering/world_map/layers/quest_poi_layer.hpp
Normal file
21
include/rendering/world_map/layers/quest_poi_layer.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// quest_poi_layer.hpp — Quest objective markers on the world map.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class QuestPOILayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void setPois(const std::vector<QuestPOI>& pois) { pois_ = &pois; }
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
private:
|
||||||
|
const std::vector<QuestPOI>* pois_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
16
include/rendering/world_map/layers/subzone_tooltip_layer.hpp
Normal file
16
include/rendering/world_map/layers/subzone_tooltip_layer.hpp
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// subzone_tooltip_layer.hpp — Overlay area hover labels in zone view.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class SubzoneTooltipLayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
21
include/rendering/world_map/layers/taxi_node_layer.hpp
Normal file
21
include/rendering/world_map/layers/taxi_node_layer.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// taxi_node_layer.hpp — Flight master diamond icons on the world map.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class TaxiNodeLayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
void setNodes(const std::vector<TaxiNode>& nodes) { nodes_ = &nodes; }
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
private:
|
||||||
|
const std::vector<TaxiNode>* nodes_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
55
include/rendering/world_map/layers/zone_highlight_layer.hpp
Normal file
55
include/rendering/world_map/layers/zone_highlight_layer.hpp
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// zone_highlight_layer.hpp — Continent view zone rectangles + hover effects.
|
||||||
|
#pragma once
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
#include "rendering/world_map/zone_metadata.hpp"
|
||||||
|
#include "rendering/vk_texture.hpp"
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
class VkContext;
|
||||||
|
}
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class ZoneHighlightLayer : public IOverlayLayer {
|
||||||
|
public:
|
||||||
|
~ZoneHighlightLayer() override;
|
||||||
|
|
||||||
|
void setMetadata(const ZoneMetadata* metadata) { metadata_ = metadata; }
|
||||||
|
void initialize(VkContext* ctx, pipeline::AssetManager* am);
|
||||||
|
void clearTextures();
|
||||||
|
void render(const LayerContext& ctx) override;
|
||||||
|
int hoveredZone() const { return hoveredZone_; }
|
||||||
|
|
||||||
|
/// Get the ImGui texture ID for a highlight BLP, loading lazily.
|
||||||
|
/// key is used as cache key; customPath overrides the default path if non-empty.
|
||||||
|
ImTextureID getHighlightTexture(const std::string& key,
|
||||||
|
const std::string& customPath = "");
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Load the highlight BLP and register it with ImGui.
|
||||||
|
void ensureHighlight(const std::string& key, const std::string& customPath);
|
||||||
|
|
||||||
|
const ZoneMetadata* metadata_ = nullptr;
|
||||||
|
VkContext* vkCtx_ = nullptr;
|
||||||
|
pipeline::AssetManager* assetManager_ = nullptr;
|
||||||
|
|
||||||
|
struct HighlightEntry {
|
||||||
|
std::unique_ptr<VkTexture> texture;
|
||||||
|
VkDescriptorSet imguiDS = VK_NULL_HANDLE; // ImGui texture ID
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, HighlightEntry> highlights_;
|
||||||
|
std::unordered_set<std::string> missingHighlights_; // areas with no highlight file
|
||||||
|
|
||||||
|
int hoveredZone_ = -1;
|
||||||
|
int prevHoveredZone_ = -1;
|
||||||
|
float hoverHighlightAlpha_ = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
75
include/rendering/world_map/map_resolver.hpp
Normal file
75
include/rendering/world_map/map_resolver.hpp
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
// map_resolver.hpp — Centralized map navigation resolution for the world map.
|
||||||
|
// Determines the correct action when clicking a region or zone at any view level.
|
||||||
|
// All functions are stateless free functions — trivially testable.
|
||||||
|
// Map folder names are resolved from a built-in table matching
|
||||||
|
// Data/interface/worldmap/ rather than WorldLoader::mapIdToName.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
// ── Map folder lookup (replaces WorldLoader::mapIdToName for world map) ──
|
||||||
|
|
||||||
|
/// Map ID → worldmap folder name (e.g. 0 → "Azeroth", 571 → "Northrend").
|
||||||
|
/// Returns empty string if unknown.
|
||||||
|
const char* mapIdToFolder(uint32_t mapId);
|
||||||
|
|
||||||
|
/// Worldmap folder name → map ID (e.g. "Azeroth" → 0, "Northrend" → 571).
|
||||||
|
/// Case-insensitive comparison. Returns -1 if unknown.
|
||||||
|
int folderToMapId(const std::string& folder);
|
||||||
|
|
||||||
|
/// Map ID → display name for UI (e.g. 0 → "Eastern Kingdoms", 571 → "Northrend").
|
||||||
|
/// Returns nullptr if unknown.
|
||||||
|
const char* mapDisplayName(uint32_t mapId);
|
||||||
|
|
||||||
|
// ── Result types ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
enum class MapResolveAction {
|
||||||
|
NONE, ///< No valid navigation target
|
||||||
|
NAVIGATE_CONTINENT, ///< Switch to continent view within current map data
|
||||||
|
LOAD_MAP, ///< Load a different map entirely (switchToMap)
|
||||||
|
ENTER_ZONE, ///< Enter zone view within current continent
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MapResolveResult {
|
||||||
|
MapResolveAction action = MapResolveAction::NONE;
|
||||||
|
int targetZoneIdx = -1; ///< Zone index for NAVIGATE_CONTINENT or ENTER_ZONE
|
||||||
|
std::string targetMapName; ///< Map folder name for LOAD_MAP
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Resolve functions ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Resolve WORLD view region click. Determines whether to navigate within
|
||||||
|
/// the current map data (e.g. clicking EK when already on Azeroth) or load
|
||||||
|
/// a new map (e.g. clicking Kalimdor or Northrend from Azeroth world view).
|
||||||
|
MapResolveResult resolveWorldRegionClick(uint32_t regionMapId,
|
||||||
|
const std::vector<Zone>& zones,
|
||||||
|
int currentMapId,
|
||||||
|
int cosmicIdx);
|
||||||
|
|
||||||
|
/// Resolve CONTINENT view zone click. Determines whether the clicked zone
|
||||||
|
/// can be entered directly (same map) or requires loading a different map
|
||||||
|
/// (zone's displayMapID differs from current).
|
||||||
|
MapResolveResult resolveZoneClick(int zoneIdx,
|
||||||
|
const std::vector<Zone>& zones,
|
||||||
|
int currentMapId);
|
||||||
|
|
||||||
|
/// Resolve COSMIC view map click. Always returns LOAD_MAP for the target.
|
||||||
|
MapResolveResult resolveCosmicClick(uint32_t targetMapId);
|
||||||
|
|
||||||
|
/// Find the best continent zone index to display for a given mapId within
|
||||||
|
/// the currently loaded zones. Prefers leaf continents over root continents.
|
||||||
|
/// Returns -1 if no suitable continent is found.
|
||||||
|
int findContinentForMapId(const std::vector<Zone>& zones,
|
||||||
|
uint32_t mapId,
|
||||||
|
int cosmicIdx);
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
71
include/rendering/world_map/overlay_renderer.hpp
Normal file
71
include/rendering/world_map/overlay_renderer.hpp
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
// overlay_renderer.hpp — ImGui overlay layer system for the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
// OCP — new marker types are added by implementing IOverlayLayer.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
struct ImDrawList;
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
/// Context passed to each overlay layer during rendering.
|
||||||
|
struct LayerContext {
|
||||||
|
ImDrawList* drawList = nullptr;
|
||||||
|
ImVec2 imgMin; // top-left of map image in screen space
|
||||||
|
float displayW = 0, displayH = 0;
|
||||||
|
glm::vec3 playerRenderPos;
|
||||||
|
float playerYawDeg = 0;
|
||||||
|
int currentZoneIdx = -1;
|
||||||
|
int continentIdx = -1;
|
||||||
|
int currentMapId = -1;
|
||||||
|
ViewLevel viewLevel = ViewLevel::ZONE;
|
||||||
|
const std::vector<Zone>* zones = nullptr;
|
||||||
|
const std::unordered_set<int>* exploredZones = nullptr;
|
||||||
|
const std::unordered_set<int>* exploredOverlays = nullptr;
|
||||||
|
const std::unordered_map<uint32_t, std::string>* areaNameByAreaId = nullptr;
|
||||||
|
// FBO dimensions for overlay coordinate math
|
||||||
|
int fboW = 1024;
|
||||||
|
int fboH = 768;
|
||||||
|
|
||||||
|
// ZMP pixel map for continent-view hover (128x128 grid of AreaTable IDs)
|
||||||
|
const std::array<uint32_t, 128 * 128>* zmpGrid = nullptr;
|
||||||
|
bool hasZmpData = false;
|
||||||
|
// Function to resolve AreaTable ID → zone index (from DataRepository)
|
||||||
|
int (*zmpResolveZoneIdx)(const void* repo, uint32_t areaId) = nullptr;
|
||||||
|
const void* zmpRepoPtr = nullptr; // opaque DataRepository pointer
|
||||||
|
// ZMP-derived zone bounding boxes (zone index → UV rect on display)
|
||||||
|
const std::unordered_map<int, ZmpRect>* zmpZoneBounds = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Interface for an overlay layer rendered on top of the composite map.
|
||||||
|
class IOverlayLayer {
|
||||||
|
public:
|
||||||
|
virtual ~IOverlayLayer() = default;
|
||||||
|
virtual void render(const LayerContext& ctx) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Orchestrates rendering of all registered overlay layers.
|
||||||
|
class OverlayRenderer {
|
||||||
|
public:
|
||||||
|
void addLayer(std::unique_ptr<IOverlayLayer> layer);
|
||||||
|
void render(const LayerContext& ctx);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::unique_ptr<IOverlayLayer>> layers_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
65
include/rendering/world_map/view_state_machine.hpp
Normal file
65
include/rendering/world_map/view_state_machine.hpp
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// view_state_machine.hpp — Navigation state and transitions for the world map.
|
||||||
|
// Extracted from WorldMap zoom/enter methods (Phase 6 of refactoring plan).
|
||||||
|
// SRP — pure state machine, no rendering or input code.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
/// Manages the current view level and transitions between views.
|
||||||
|
class ViewStateMachine {
|
||||||
|
public:
|
||||||
|
ViewLevel currentLevel() const { return level_; }
|
||||||
|
const TransitionState& transition() const { return transition_; }
|
||||||
|
|
||||||
|
int continentIdx() const { return continentIdx_; }
|
||||||
|
int currentZoneIdx() const { return currentIdx_; }
|
||||||
|
bool cosmicEnabled() const { return cosmicEnabled_; }
|
||||||
|
|
||||||
|
void setContinentIdx(int idx) { continentIdx_ = idx; }
|
||||||
|
void setCurrentZoneIdx(int idx) { currentIdx_ = idx; }
|
||||||
|
void setCosmicEnabled(bool enabled) { cosmicEnabled_ = enabled; }
|
||||||
|
void setLevel(ViewLevel level) { level_ = level; }
|
||||||
|
|
||||||
|
/// Result of a zoom/navigate operation.
|
||||||
|
struct ZoomResult {
|
||||||
|
bool changed = false;
|
||||||
|
ViewLevel newLevel = ViewLevel::ZONE;
|
||||||
|
int targetIdx = -1; // zone index to load/composite
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Attempt to zoom in. hoveredZoneIdx is the zone under the cursor (-1 if none).
|
||||||
|
/// playerZoneIdx is the zone the player is standing in (-1 if none).
|
||||||
|
ZoomResult zoomIn(int hoveredZoneIdx, int playerZoneIdx);
|
||||||
|
|
||||||
|
/// Attempt to zoom out one level.
|
||||||
|
ZoomResult zoomOut();
|
||||||
|
|
||||||
|
/// Navigate to world view. Returns the root/fallback continent index to composite.
|
||||||
|
ZoomResult enterWorldView();
|
||||||
|
|
||||||
|
/// Navigate to cosmic view.
|
||||||
|
ZoomResult enterCosmicView();
|
||||||
|
|
||||||
|
/// Navigate directly into a zone from continent view.
|
||||||
|
ZoomResult enterZone(int zoneIdx);
|
||||||
|
|
||||||
|
/// Advance transition animation. Returns true while animating.
|
||||||
|
bool updateTransition(float deltaTime);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void startTransition(ViewLevel from, ViewLevel to, float duration = 0.3f);
|
||||||
|
|
||||||
|
ViewLevel level_ = ViewLevel::CONTINENT;
|
||||||
|
TransitionState transition_;
|
||||||
|
int continentIdx_ = -1;
|
||||||
|
int currentIdx_ = -1;
|
||||||
|
bool cosmicEnabled_ = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
64
include/rendering/world_map/world_map_facade.hpp
Normal file
64
include/rendering/world_map/world_map_facade.hpp
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
// world_map_facade.hpp — Public API for the world map system.
|
||||||
|
// Drop-in replacement for the monolithic WorldMap class (Phase 10 of refactoring plan).
|
||||||
|
// Facade pattern — hides internal complexity behind the same public interface.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
class VkContext;
|
||||||
|
}
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class WorldMapFacade {
|
||||||
|
public:
|
||||||
|
/// Backward-compatible alias for old WorldMap::QuestPoi usage.
|
||||||
|
using QuestPoi = QuestPOI;
|
||||||
|
|
||||||
|
WorldMapFacade();
|
||||||
|
~WorldMapFacade();
|
||||||
|
|
||||||
|
bool initialize(VkContext* ctx, pipeline::AssetManager* am);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
/// Off-screen composite pass — call BEFORE the main render pass begins.
|
||||||
|
void compositePass(VkCommandBuffer cmd);
|
||||||
|
|
||||||
|
/// ImGui overlay — call INSIDE the main render pass (during ImGui frame).
|
||||||
|
void render(const glm::vec3& playerRenderPos,
|
||||||
|
int screenWidth, int screenHeight,
|
||||||
|
float playerYawDeg = 0.0f);
|
||||||
|
|
||||||
|
void setMapName(const std::string& name);
|
||||||
|
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
||||||
|
void setPartyDots(std::vector<PartyDot> dots);
|
||||||
|
void setTaxiNodes(std::vector<TaxiNode> nodes);
|
||||||
|
void setQuestPois(std::vector<QuestPOI> pois);
|
||||||
|
void setCorpsePos(bool hasCorpse, glm::vec3 renderPos);
|
||||||
|
|
||||||
|
bool isOpen() const;
|
||||||
|
void close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
|
|
||||||
|
// Backward-compatible alias for gradual migration
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
using WorldMap = world_map::WorldMapFacade;
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
132
include/rendering/world_map/world_map_types.hpp
Normal file
132
include/rendering/world_map/world_map_types.hpp
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
// world_map_types.hpp — Vulkan-free domain types for the world map system.
|
||||||
|
// Extracted from rendering/world_map.hpp (Phase 1 of refactoring plan).
|
||||||
|
// Consumers of these types do NOT need Vulkan/VMA headers.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
// ── View hierarchy ───────────────────────────────────────────
|
||||||
|
|
||||||
|
enum class ViewLevel { COSMIC, WORLD, CONTINENT, ZONE };
|
||||||
|
|
||||||
|
// ── Transition animation ─────────────────────────────────────
|
||||||
|
|
||||||
|
struct TransitionState {
|
||||||
|
bool active = false;
|
||||||
|
float progress = 0.0f; // 0.0 → 1.0
|
||||||
|
float duration = 0.2f; // seconds
|
||||||
|
ViewLevel fromLevel = ViewLevel::ZONE;
|
||||||
|
ViewLevel toLevel = ViewLevel::ZONE;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Zone faction & metadata ──────────────────────────────────
|
||||||
|
|
||||||
|
enum class ZoneFaction : uint8_t { Neutral, Alliance, Horde, Contested };
|
||||||
|
|
||||||
|
struct ZoneMeta {
|
||||||
|
uint8_t minLevel = 0, maxLevel = 0;
|
||||||
|
ZoneFaction faction = ZoneFaction::Neutral;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Cosmic view (cross-realm) ────────────────────────────────
|
||||||
|
|
||||||
|
struct CosmicMapEntry {
|
||||||
|
int mapId = 0;
|
||||||
|
std::string label;
|
||||||
|
// Clickable region in UV space (0-1 range on the cosmic composite)
|
||||||
|
float uvLeft = 0, uvTop = 0, uvRight = 0, uvBottom = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Zone bounds (shared between Zone and coordinate projection) ──
|
||||||
|
|
||||||
|
struct ZoneBounds {
|
||||||
|
float locLeft = 0, locRight = 0;
|
||||||
|
float locTop = 0, locBottom = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// ZMP-derived bounding rectangle in display UV [0,1] coordinates.
|
||||||
|
/// Computed by scanning the ZMP grid for each zone's area ID.
|
||||||
|
/// Maps pixel-for-pixel to the continent map tiles shown on screen.
|
||||||
|
struct ZmpRect {
|
||||||
|
float uMin = 0, uMax = 0;
|
||||||
|
float vMin = 0, vMax = 0;
|
||||||
|
bool valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Overlay entry (exploration overlay from WorldMapOverlay.dbc) ──
|
||||||
|
|
||||||
|
struct OverlayEntry {
|
||||||
|
uint32_t areaIDs[4] = {}; // Up to 4 AreaTable IDs contributing to this overlay
|
||||||
|
std::string textureName; // Texture prefix (e.g., "Goldshire")
|
||||||
|
uint16_t texWidth = 0, texHeight = 0; // Overlay size in pixels
|
||||||
|
uint16_t offsetX = 0, offsetY = 0; // Pixel offset within zone map
|
||||||
|
int tileCols = 0, tileRows = 0;
|
||||||
|
// HitRect from WorldMapOverlay.dbc fields 13-16 — fast AABB pre-filter for
|
||||||
|
// subzone hover detection in zone view (avoids sampling every overlay).
|
||||||
|
uint16_t hitRectLeft = 0, hitRectRight = 0;
|
||||||
|
uint16_t hitRectTop = 0, hitRectBottom = 0;
|
||||||
|
// NOTE: texture pointers are managed by CompositeRenderer, not stored here.
|
||||||
|
bool tilesLoaded = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Zone (from WorldMapArea.dbc) ─────────────────────────────
|
||||||
|
|
||||||
|
struct Zone {
|
||||||
|
uint32_t wmaID = 0;
|
||||||
|
uint32_t areaID = 0; // 0 = continent level
|
||||||
|
std::string areaName; // texture folder name (from DBC)
|
||||||
|
ZoneBounds bounds;
|
||||||
|
uint32_t displayMapID = 0;
|
||||||
|
uint32_t parentWorldMapID = 0;
|
||||||
|
std::vector<uint32_t> exploreBits; // all AreaBit indices (zone + subzones)
|
||||||
|
std::vector<OverlayEntry> overlays;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Party member dot (UI layer → world map overlay) ──────────
|
||||||
|
|
||||||
|
struct PartyDot {
|
||||||
|
glm::vec3 renderPos; ///< Position in render-space coordinates
|
||||||
|
uint32_t color; ///< RGBA packed color (IM_COL32 format)
|
||||||
|
std::string name; ///< Member name (shown as tooltip on hover)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Taxi (flight master) node (UI layer → world map overlay) ─
|
||||||
|
|
||||||
|
struct TaxiNode {
|
||||||
|
uint32_t id = 0; ///< TaxiNodes.dbc ID
|
||||||
|
uint32_t mapId = 0; ///< WoW internal map ID (0=EK,1=Kal,530=Outland,571=Northrend)
|
||||||
|
float wowX = 0, wowY = 0, wowZ = 0; ///< Canonical WoW coordinates
|
||||||
|
std::string name; ///< Node name (shown as tooltip)
|
||||||
|
bool known = false; ///< Player has discovered this node
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Area Point of Interest from AreaPOI.dbc ──────────────────
|
||||||
|
|
||||||
|
struct POI {
|
||||||
|
uint32_t id = 0;
|
||||||
|
uint32_t importance = 0; ///< 0=small, 1=medium, 2=large (capital)
|
||||||
|
uint32_t iconType = 0; ///< Icon category from AreaPOI.dbc
|
||||||
|
uint32_t factionId = 0; ///< 0=neutral, 67=Horde, 469=Alliance
|
||||||
|
float wowX = 0, wowY = 0, wowZ = 0; ///< Canonical WoW coordinates
|
||||||
|
uint32_t mapId = 0; ///< WoW internal map ID
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Quest POI marker (from SMSG_QUEST_POI_QUERY_RESPONSE) ────
|
||||||
|
|
||||||
|
struct QuestPOI {
|
||||||
|
float wowX = 0, wowY = 0; ///< Canonical WoW coordinates (centroid)
|
||||||
|
std::string name; ///< Quest title
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
37
include/rendering/world_map/zone_metadata.hpp
Normal file
37
include/rendering/world_map/zone_metadata.hpp
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// zone_metadata.hpp — Zone level ranges, faction data, and label formatting.
|
||||||
|
// Extracted from WorldMap::initZoneMeta and inline label formatting
|
||||||
|
// (Phase 4 of refactoring plan). DRY — formatLabel used by multiple layers.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
class ZoneMetadata {
|
||||||
|
public:
|
||||||
|
/// Initialize the zone metadata table (level ranges, factions).
|
||||||
|
void initialize();
|
||||||
|
|
||||||
|
/// Look up metadata for a zone by area name. Returns nullptr if not found.
|
||||||
|
const ZoneMeta* find(const std::string& areaName) const;
|
||||||
|
|
||||||
|
/// Format a zone label with level range and faction tag.
|
||||||
|
/// e.g. "Elwynn (1-10) [Alliance]"
|
||||||
|
static std::string formatLabel(const std::string& areaName,
|
||||||
|
const ZoneMeta* meta);
|
||||||
|
|
||||||
|
/// Format hover label with level range and bracket-tag for faction.
|
||||||
|
static std::string formatHoverLabel(const std::string& areaName,
|
||||||
|
const ZoneMeta* meta);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, ZoneMeta> table_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -71,6 +71,8 @@ const char* WorldLoader::mapDisplayName(uint32_t mapId) {
|
||||||
switch (mapId) {
|
switch (mapId) {
|
||||||
case 0: return "Eastern Kingdoms";
|
case 0: return "Eastern Kingdoms";
|
||||||
case 1: return "Kalimdor";
|
case 1: return "Kalimdor";
|
||||||
|
case 13: return "Test";
|
||||||
|
case 169: return "Emerald Dream";
|
||||||
case 530: return "Outland";
|
case 530: return "Outland";
|
||||||
case 571: return "Northrend";
|
case 571: return "Northrend";
|
||||||
default: return nullptr;
|
default: return nullptr;
|
||||||
|
|
@ -170,6 +172,15 @@ const char* WorldLoader::mapIdToName(uint32_t mapId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int WorldLoader::mapNameToId(const std::string& name) {
|
||||||
|
// Reverse lookup: iterate known continent IDs and match against mapIdToName.
|
||||||
|
static constexpr uint32_t kContinentIds[] = {0, 1, 530, 571};
|
||||||
|
for (uint32_t id : kContinentIds) {
|
||||||
|
if (name == mapIdToName(id)) return static_cast<int>(id);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
void WorldLoader::processPendingEntry() {
|
void WorldLoader::processPendingEntry() {
|
||||||
if (!pendingWorldEntry_ || loadingWorld_) return;
|
if (!pendingWorldEntry_ || loadingWorld_) return;
|
||||||
auto entry = *pendingWorldEntry_;
|
auto entry = *pendingWorldEntry_;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
570
src/rendering/world_map/composite_renderer.cpp
Normal file
570
src/rendering/world_map/composite_renderer.cpp
Normal file
|
|
@ -0,0 +1,570 @@
|
||||||
|
// composite_renderer.cpp — Vulkan off-screen composite rendering for the world map.
|
||||||
|
// Extracted from WorldMap::initialize, shutdown, compositePass, loadZoneTextures,
|
||||||
|
// loadOverlayTextures, destroyZoneTextures (Phase 7 of refactoring plan).
|
||||||
|
#include "rendering/world_map/composite_renderer.hpp"
|
||||||
|
#include "rendering/vk_context.hpp"
|
||||||
|
#include "rendering/vk_texture.hpp"
|
||||||
|
#include "rendering/vk_render_target.hpp"
|
||||||
|
#include "rendering/vk_pipeline.hpp"
|
||||||
|
#include "rendering/vk_shader.hpp"
|
||||||
|
#include "rendering/vk_utils.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
CompositeRenderer::CompositeRenderer() = default;
|
||||||
|
|
||||||
|
CompositeRenderer::~CompositeRenderer() {
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::ensureTextureSlots(size_t zoneCount, const std::vector<Zone>& zones) {
|
||||||
|
if (zoneTextureSlots_.size() >= zoneCount) return;
|
||||||
|
zoneTextureSlots_.resize(zoneCount);
|
||||||
|
for (size_t i = 0; i < zoneCount; i++) {
|
||||||
|
auto& slots = zoneTextureSlots_[i];
|
||||||
|
if (slots.overlays.size() != zones[i].overlays.size()) {
|
||||||
|
slots.overlays.resize(zones[i].overlays.size());
|
||||||
|
for (size_t oi = 0; oi < zones[i].overlays.size(); oi++) {
|
||||||
|
const auto& ov = zones[i].overlays[oi];
|
||||||
|
slots.overlays[oi].tiles.resize(ov.tileCols * ov.tileRows, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CompositeRenderer::initialize(VkContext* ctx, pipeline::AssetManager* am) {
|
||||||
|
if (initialized) return true;
|
||||||
|
vkCtx = ctx;
|
||||||
|
assetManager = am;
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
|
||||||
|
// --- Composite render target (1024x768) ---
|
||||||
|
compositeTarget = std::make_unique<VkRenderTarget>();
|
||||||
|
if (!compositeTarget->create(*vkCtx, FBO_W, FBO_H)) {
|
||||||
|
LOG_ERROR("CompositeRenderer: failed to create composite render target");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Quad vertex buffer (unit quad: pos2 + uv2) ---
|
||||||
|
float quadVerts[] = {
|
||||||
|
0.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
1.0f, 0.0f, 1.0f, 0.0f,
|
||||||
|
1.0f, 1.0f, 1.0f, 1.0f,
|
||||||
|
0.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
1.0f, 1.0f, 1.0f, 1.0f,
|
||||||
|
0.0f, 1.0f, 0.0f, 1.0f,
|
||||||
|
};
|
||||||
|
auto quadBuf = uploadBuffer(*vkCtx, quadVerts, sizeof(quadVerts),
|
||||||
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||||
|
quadVB = quadBuf.buffer;
|
||||||
|
quadVBAlloc = quadBuf.allocation;
|
||||||
|
|
||||||
|
// --- Descriptor set layout: 1 combined image sampler at binding 0 ---
|
||||||
|
VkDescriptorSetLayoutBinding samplerBinding{};
|
||||||
|
samplerBinding.binding = 0;
|
||||||
|
samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
samplerBinding.descriptorCount = 1;
|
||||||
|
samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
|
samplerSetLayout = createDescriptorSetLayout(device, { samplerBinding });
|
||||||
|
|
||||||
|
// --- Descriptor pool ---
|
||||||
|
VkDescriptorPoolSize poolSize{};
|
||||||
|
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
poolSize.descriptorCount = MAX_DESC_SETS;
|
||||||
|
|
||||||
|
VkDescriptorPoolCreateInfo poolInfo{};
|
||||||
|
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||||
|
poolInfo.maxSets = MAX_DESC_SETS;
|
||||||
|
poolInfo.poolSizeCount = 1;
|
||||||
|
poolInfo.pPoolSizes = &poolSize;
|
||||||
|
vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool);
|
||||||
|
|
||||||
|
// --- Allocate descriptor sets ---
|
||||||
|
constexpr uint32_t tileSetCount = 24;
|
||||||
|
constexpr uint32_t overlaySetCount = MAX_OVERLAY_TILES * 2;
|
||||||
|
constexpr uint32_t totalSets = tileSetCount + 1 + 1 + overlaySetCount;
|
||||||
|
std::vector<VkDescriptorSetLayout> layouts(totalSets, samplerSetLayout);
|
||||||
|
VkDescriptorSetAllocateInfo allocInfo{};
|
||||||
|
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||||
|
allocInfo.descriptorPool = descPool;
|
||||||
|
allocInfo.descriptorSetCount = totalSets;
|
||||||
|
allocInfo.pSetLayouts = layouts.data();
|
||||||
|
|
||||||
|
std::vector<VkDescriptorSet> allSets(totalSets);
|
||||||
|
vkAllocateDescriptorSets(device, &allocInfo, allSets.data());
|
||||||
|
|
||||||
|
uint32_t si = 0;
|
||||||
|
for (int f = 0; f < 2; f++)
|
||||||
|
for (int t = 0; t < 12; t++)
|
||||||
|
tileDescSets[f][t] = allSets[si++];
|
||||||
|
imguiDisplaySet = allSets[si++];
|
||||||
|
fogDescSet_ = allSets[si++];
|
||||||
|
for (int f = 0; f < 2; f++)
|
||||||
|
for (uint32_t t = 0; t < MAX_OVERLAY_TILES; t++)
|
||||||
|
overlayDescSets_[f][t] = allSets[si++];
|
||||||
|
|
||||||
|
// --- Write display descriptor set → composite render target ---
|
||||||
|
VkDescriptorImageInfo compositeImgInfo = compositeTarget->descriptorInfo();
|
||||||
|
VkWriteDescriptorSet displayWrite{};
|
||||||
|
displayWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
displayWrite.dstSet = imguiDisplaySet;
|
||||||
|
displayWrite.dstBinding = 0;
|
||||||
|
displayWrite.descriptorCount = 1;
|
||||||
|
displayWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
displayWrite.pImageInfo = &compositeImgInfo;
|
||||||
|
vkUpdateDescriptorSets(device, 1, &displayWrite, 0, nullptr);
|
||||||
|
|
||||||
|
// --- Pipeline layout: samplerSetLayout + push constant (16 bytes, vertex) ---
|
||||||
|
VkPushConstantRange tilePush{};
|
||||||
|
tilePush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
||||||
|
tilePush.offset = 0;
|
||||||
|
tilePush.size = sizeof(WorldMapTilePush);
|
||||||
|
tilePipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { tilePush });
|
||||||
|
|
||||||
|
// --- Vertex input: pos2 (loc 0) + uv2 (loc 1), stride 16 ---
|
||||||
|
VkVertexInputBindingDescription binding{};
|
||||||
|
binding.binding = 0;
|
||||||
|
binding.stride = 4 * sizeof(float);
|
||||||
|
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
|
|
||||||
|
std::vector<VkVertexInputAttributeDescription> attrs(2);
|
||||||
|
attrs[0] = { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 };
|
||||||
|
attrs[1] = { 1, 0, VK_FORMAT_R32G32_SFLOAT, 2 * sizeof(float) };
|
||||||
|
|
||||||
|
// --- Load tile shaders and build pipeline ---
|
||||||
|
{
|
||||||
|
VkShaderModule vs, fs;
|
||||||
|
if (!vs.loadFromFile(device, "assets/shaders/world_map.vert.spv") ||
|
||||||
|
!fs.loadFromFile(device, "assets/shaders/world_map.frag.spv")) {
|
||||||
|
LOG_ERROR("CompositeRenderer: failed to load tile shaders");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tilePipeline = PipelineBuilder()
|
||||||
|
.setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||||
|
fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||||
|
.setVertexInput({ binding }, attrs)
|
||||||
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||||
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||||
|
.setNoDepthTest()
|
||||||
|
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
||||||
|
.setLayout(tilePipelineLayout)
|
||||||
|
.setRenderPass(compositeTarget->getRenderPass())
|
||||||
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
||||||
|
.build(device, vkCtx->getPipelineCache());
|
||||||
|
|
||||||
|
vs.destroy();
|
||||||
|
fs.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tilePipeline) {
|
||||||
|
LOG_ERROR("CompositeRenderer: failed to create tile pipeline");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Overlay pipeline (alpha-blended) ---
|
||||||
|
{
|
||||||
|
VkPushConstantRange overlayPushVert{};
|
||||||
|
overlayPushVert.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
||||||
|
overlayPushVert.offset = 0;
|
||||||
|
overlayPushVert.size = sizeof(WorldMapTilePush);
|
||||||
|
|
||||||
|
VkPushConstantRange overlayPushFrag{};
|
||||||
|
overlayPushFrag.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
|
overlayPushFrag.offset = 16;
|
||||||
|
overlayPushFrag.size = sizeof(glm::vec4);
|
||||||
|
|
||||||
|
overlayPipelineLayout_ = createPipelineLayout(device, { samplerSetLayout },
|
||||||
|
{ overlayPushVert, overlayPushFrag });
|
||||||
|
|
||||||
|
VkShaderModule vs, fs;
|
||||||
|
if (!vs.loadFromFile(device, "assets/shaders/world_map.vert.spv") ||
|
||||||
|
!fs.loadFromFile(device, "assets/shaders/world_map_fog.frag.spv")) {
|
||||||
|
LOG_ERROR("CompositeRenderer: failed to load overlay shaders");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayPipeline_ = PipelineBuilder()
|
||||||
|
.setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||||
|
fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||||
|
.setVertexInput({ binding }, attrs)
|
||||||
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||||
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||||
|
.setNoDepthTest()
|
||||||
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
||||||
|
.setLayout(overlayPipelineLayout_)
|
||||||
|
.setRenderPass(compositeTarget->getRenderPass())
|
||||||
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
||||||
|
.build(device, vkCtx->getPipelineCache());
|
||||||
|
|
||||||
|
vs.destroy();
|
||||||
|
fs.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overlayPipeline_) {
|
||||||
|
LOG_ERROR("CompositeRenderer: failed to create overlay pipeline");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 1×1 white fog texture ---
|
||||||
|
{
|
||||||
|
uint8_t white[] = { 255, 255, 255, 255 };
|
||||||
|
fogTexture_ = std::make_unique<VkTexture>();
|
||||||
|
fogTexture_->upload(*vkCtx, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||||
|
fogTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST,
|
||||||
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
||||||
|
|
||||||
|
VkDescriptorImageInfo fogImgInfo = fogTexture_->descriptorInfo();
|
||||||
|
VkWriteDescriptorSet fogWrite{};
|
||||||
|
fogWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
fogWrite.dstSet = fogDescSet_;
|
||||||
|
fogWrite.dstBinding = 0;
|
||||||
|
fogWrite.descriptorCount = 1;
|
||||||
|
fogWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
fogWrite.pImageInfo = &fogImgInfo;
|
||||||
|
vkUpdateDescriptorSets(device, 1, &fogWrite, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
LOG_INFO("CompositeRenderer initialized (", FBO_W, "x", FBO_H, " composite)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::shutdown() {
|
||||||
|
if (!vkCtx) return;
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
VmaAllocator alloc = vkCtx->getAllocator();
|
||||||
|
|
||||||
|
vkDeviceWaitIdle(device);
|
||||||
|
|
||||||
|
if (tilePipeline) { vkDestroyPipeline(device, tilePipeline, nullptr); tilePipeline = VK_NULL_HANDLE; }
|
||||||
|
if (tilePipelineLayout) { vkDestroyPipelineLayout(device, tilePipelineLayout, nullptr); tilePipelineLayout = VK_NULL_HANDLE; }
|
||||||
|
if (overlayPipeline_) { vkDestroyPipeline(device, overlayPipeline_, nullptr); overlayPipeline_ = VK_NULL_HANDLE; }
|
||||||
|
if (overlayPipelineLayout_) { vkDestroyPipelineLayout(device, overlayPipelineLayout_, nullptr); overlayPipelineLayout_ = VK_NULL_HANDLE; }
|
||||||
|
if (descPool) { vkDestroyDescriptorPool(device, descPool, nullptr); descPool = VK_NULL_HANDLE; }
|
||||||
|
if (samplerSetLayout) { vkDestroyDescriptorSetLayout(device, samplerSetLayout, nullptr); samplerSetLayout = VK_NULL_HANDLE; }
|
||||||
|
if (quadVB) { vmaDestroyBuffer(alloc, quadVB, quadVBAlloc); quadVB = VK_NULL_HANDLE; }
|
||||||
|
|
||||||
|
for (auto& tex : zoneTextures) {
|
||||||
|
if (tex) tex->destroy(device, alloc);
|
||||||
|
}
|
||||||
|
zoneTextures.clear();
|
||||||
|
zoneTextureSlots_.clear();
|
||||||
|
|
||||||
|
if (fogTexture_) { fogTexture_->destroy(device, alloc); fogTexture_.reset(); }
|
||||||
|
if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); }
|
||||||
|
|
||||||
|
initialized = false;
|
||||||
|
vkCtx = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::destroyZoneTextures(std::vector<Zone>& /*zones*/) {
|
||||||
|
if (!vkCtx) return;
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
VmaAllocator alloc = vkCtx->getAllocator();
|
||||||
|
|
||||||
|
for (auto& tex : zoneTextures) {
|
||||||
|
if (tex) tex->destroy(device, alloc);
|
||||||
|
}
|
||||||
|
zoneTextures.clear();
|
||||||
|
|
||||||
|
for (auto& slots : zoneTextureSlots_) {
|
||||||
|
for (auto& tex : slots.tileTextures) tex = nullptr;
|
||||||
|
slots.tilesLoaded = false;
|
||||||
|
for (auto& ov : slots.overlays) {
|
||||||
|
for (auto& t : ov.tiles) t = nullptr;
|
||||||
|
ov.tilesLoaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zoneTextureSlots_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::loadZoneTextures(int zoneIdx, std::vector<Zone>& zones,
|
||||||
|
const std::string& mapName) {
|
||||||
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return;
|
||||||
|
ensureTextureSlots(zones.size(), zones);
|
||||||
|
auto& slots = zoneTextureSlots_[zoneIdx];
|
||||||
|
if (slots.tilesLoaded) return;
|
||||||
|
slots.tilesLoaded = true;
|
||||||
|
|
||||||
|
const auto& zone = zones[zoneIdx];
|
||||||
|
const std::string& folder = zone.areaName;
|
||||||
|
if (folder.empty()) return;
|
||||||
|
|
||||||
|
LOG_INFO("loadZoneTextures: zone[", zoneIdx, "] areaName='", zone.areaName,
|
||||||
|
"' areaID=", zone.areaID, " mapName='", mapName, "'");
|
||||||
|
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
int loaded = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
std::string path = "Interface\\WorldMap\\" + folder + "\\" +
|
||||||
|
folder + std::to_string(i + 1) + ".blp";
|
||||||
|
auto blpImage = assetManager->loadTexture(path);
|
||||||
|
if (!blpImage.isValid()) {
|
||||||
|
slots.tileTextures[i] = nullptr;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto tex = std::make_unique<VkTexture>();
|
||||||
|
tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height,
|
||||||
|
VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||||
|
tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
||||||
|
|
||||||
|
slots.tileTextures[i] = tex.get();
|
||||||
|
zoneTextures.push_back(std::move(tex));
|
||||||
|
loaded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("CompositeRenderer: loaded ", loaded, "/12 tiles for '", folder, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::loadOverlayTextures(int zoneIdx, std::vector<Zone>& zones) {
|
||||||
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return;
|
||||||
|
ensureTextureSlots(zones.size(), zones);
|
||||||
|
|
||||||
|
const auto& zone = zones[zoneIdx];
|
||||||
|
auto& slots = zoneTextureSlots_[zoneIdx];
|
||||||
|
if (zone.overlays.empty()) return;
|
||||||
|
|
||||||
|
const std::string& folder = zone.areaName;
|
||||||
|
if (folder.empty()) return;
|
||||||
|
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
int totalLoaded = 0;
|
||||||
|
|
||||||
|
for (size_t oi = 0; oi < zone.overlays.size(); oi++) {
|
||||||
|
const auto& ov = zone.overlays[oi];
|
||||||
|
auto& ovSlots = slots.overlays[oi];
|
||||||
|
if (ovSlots.tilesLoaded) continue;
|
||||||
|
ovSlots.tilesLoaded = true;
|
||||||
|
|
||||||
|
int tileCount = ov.tileCols * ov.tileRows;
|
||||||
|
for (int t = 0; t < tileCount; t++) {
|
||||||
|
std::string tileName = ov.textureName + std::to_string(t + 1);
|
||||||
|
std::string path = "Interface\\WorldMap\\" + folder + "\\" + tileName + ".blp";
|
||||||
|
auto blpImage = assetManager->loadTexture(path);
|
||||||
|
if (!blpImage.isValid()) {
|
||||||
|
ovSlots.tiles[t] = nullptr;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto tex = std::make_unique<VkTexture>();
|
||||||
|
tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height,
|
||||||
|
VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||||
|
tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
||||||
|
|
||||||
|
ovSlots.tiles[t] = tex.get();
|
||||||
|
zoneTextures.push_back(std::move(tex));
|
||||||
|
totalLoaded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("CompositeRenderer: loaded ", totalLoaded, " overlay tiles for '", folder, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::detachZoneTextures() {
|
||||||
|
if (!zoneTextures.empty() && vkCtx) {
|
||||||
|
// Defer destruction until all in-flight frames have completed.
|
||||||
|
// This avoids calling vkDeviceWaitIdle mid-frame, which can trigger
|
||||||
|
// driver TDR (GPU device lost) under heavy rendering load.
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
VmaAllocator alloc = vkCtx->getAllocator();
|
||||||
|
auto captured = std::make_shared<std::vector<std::unique_ptr<VkTexture>>>(
|
||||||
|
std::move(zoneTextures));
|
||||||
|
vkCtx->deferAfterAllFrameFences([device, alloc, captured]() {
|
||||||
|
for (auto& tex : *captured) {
|
||||||
|
if (tex) tex->destroy(device, alloc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
zoneTextures.clear();
|
||||||
|
|
||||||
|
// Clear CPU-side tracking immediately so new zones get fresh loads
|
||||||
|
for (auto& slots : zoneTextureSlots_) {
|
||||||
|
for (auto& tex : slots.tileTextures) tex = nullptr;
|
||||||
|
slots.tilesLoaded = false;
|
||||||
|
for (auto& ov : slots.overlays) {
|
||||||
|
for (auto& t : ov.tiles) t = nullptr;
|
||||||
|
ov.tilesLoaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zoneTextureSlots_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::flushStaleTextures() {
|
||||||
|
// No-op: texture cleanup is now handled by deferAfterAllFrameFences
|
||||||
|
// in detachZoneTextures. Kept for API compatibility.
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::requestComposite(int zoneIdx) {
|
||||||
|
pendingCompositeIdx_ = zoneIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CompositeRenderer::hasAnyTile(int zoneIdx) const {
|
||||||
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zoneTextureSlots_.size()))
|
||||||
|
return false;
|
||||||
|
const auto& slots = zoneTextureSlots_[zoneIdx];
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
if (slots.tileTextures[i] != nullptr) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompositeRenderer::compositePass(VkCommandBuffer cmd,
|
||||||
|
const std::vector<Zone>& zones,
|
||||||
|
const std::unordered_set<int>& exploredOverlays,
|
||||||
|
bool hasServerMask) {
|
||||||
|
if (!initialized || pendingCompositeIdx_ < 0 || !compositeTarget) return;
|
||||||
|
if (pendingCompositeIdx_ >= static_cast<int>(zones.size())) {
|
||||||
|
pendingCompositeIdx_ = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int zoneIdx = pendingCompositeIdx_;
|
||||||
|
pendingCompositeIdx_ = -1;
|
||||||
|
|
||||||
|
if (compositedIdx_ == zoneIdx) return;
|
||||||
|
ensureTextureSlots(zones.size(), zones);
|
||||||
|
|
||||||
|
const auto& zone = zones[zoneIdx];
|
||||||
|
const auto& slots = zoneTextureSlots_[zoneIdx];
|
||||||
|
uint32_t frameIdx = vkCtx->getCurrentFrame();
|
||||||
|
VkDevice device = vkCtx->getDevice();
|
||||||
|
|
||||||
|
// Update tile descriptor sets for this frame
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
VkTexture* tileTex = slots.tileTextures[i];
|
||||||
|
if (!tileTex || !tileTex->isValid()) continue;
|
||||||
|
|
||||||
|
VkDescriptorImageInfo imgInfo = tileTex->descriptorInfo();
|
||||||
|
VkWriteDescriptorSet write{};
|
||||||
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
write.dstSet = tileDescSets[frameIdx][i];
|
||||||
|
write.dstBinding = 0;
|
||||||
|
write.descriptorCount = 1;
|
||||||
|
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
write.pImageInfo = &imgInfo;
|
||||||
|
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin off-screen render pass
|
||||||
|
VkClearColorValue clearColor = {{ 0.05f, 0.08f, 0.12f, 1.0f }};
|
||||||
|
compositeTarget->beginPass(cmd, clearColor);
|
||||||
|
|
||||||
|
VkDeviceSize offset = 0;
|
||||||
|
vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset);
|
||||||
|
|
||||||
|
// --- Pass 1: Draw base map tiles (opaque) ---
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipeline);
|
||||||
|
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
if (!slots.tileTextures[i] || !slots.tileTextures[i]->isValid()) continue;
|
||||||
|
|
||||||
|
int col = i % GRID_COLS;
|
||||||
|
int row = i / GRID_COLS;
|
||||||
|
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
tilePipelineLayout, 0, 1,
|
||||||
|
&tileDescSets[frameIdx][i], 0, nullptr);
|
||||||
|
|
||||||
|
WorldMapTilePush push{};
|
||||||
|
push.gridOffset = glm::vec2(static_cast<float>(col), static_cast<float>(row));
|
||||||
|
push.gridCols = static_cast<float>(GRID_COLS);
|
||||||
|
push.gridRows = static_cast<float>(GRID_ROWS);
|
||||||
|
vkCmdPushConstants(cmd, tilePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT,
|
||||||
|
0, sizeof(push), &push);
|
||||||
|
|
||||||
|
vkCmdDraw(cmd, 6, 1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Draw explored overlay textures on top of the base map ---
|
||||||
|
bool hasOverlays = !zone.overlays.empty() && zone.areaID != 0;
|
||||||
|
if (hasOverlays && overlayPipeline_) {
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline_);
|
||||||
|
|
||||||
|
uint32_t descSlot = 0;
|
||||||
|
for (int oi = 0; oi < static_cast<int>(zone.overlays.size()); oi++) {
|
||||||
|
if (exploredOverlays.count(oi) == 0) continue;
|
||||||
|
const auto& ov = zone.overlays[oi];
|
||||||
|
const auto& ovSlots = slots.overlays[oi];
|
||||||
|
|
||||||
|
for (int t = 0; t < static_cast<int>(ovSlots.tiles.size()); t++) {
|
||||||
|
if (!ovSlots.tiles[t] || !ovSlots.tiles[t]->isValid()) continue;
|
||||||
|
if (descSlot >= MAX_OVERLAY_TILES) break;
|
||||||
|
|
||||||
|
VkDescriptorImageInfo ovImgInfo = ovSlots.tiles[t]->descriptorInfo();
|
||||||
|
VkWriteDescriptorSet ovWrite{};
|
||||||
|
ovWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
ovWrite.dstSet = overlayDescSets_[frameIdx][descSlot];
|
||||||
|
ovWrite.dstBinding = 0;
|
||||||
|
ovWrite.descriptorCount = 1;
|
||||||
|
ovWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
ovWrite.pImageInfo = &ovImgInfo;
|
||||||
|
vkUpdateDescriptorSets(device, 1, &ovWrite, 0, nullptr);
|
||||||
|
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
overlayPipelineLayout_, 0, 1,
|
||||||
|
&overlayDescSets_[frameIdx][descSlot], 0, nullptr);
|
||||||
|
|
||||||
|
int tileCol = t % ov.tileCols;
|
||||||
|
int tileRow = t / ov.tileCols;
|
||||||
|
|
||||||
|
float px = static_cast<float>(ov.offsetX + tileCol * TILE_PX);
|
||||||
|
float py = static_cast<float>(ov.offsetY + tileRow * TILE_PX);
|
||||||
|
|
||||||
|
OverlayPush ovPush{};
|
||||||
|
ovPush.gridOffset = glm::vec2(px / static_cast<float>(TILE_PX),
|
||||||
|
py / static_cast<float>(TILE_PX));
|
||||||
|
ovPush.gridCols = static_cast<float>(GRID_COLS);
|
||||||
|
ovPush.gridRows = static_cast<float>(GRID_ROWS);
|
||||||
|
ovPush.tintColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||||
|
0, sizeof(WorldMapTilePush), &ovPush);
|
||||||
|
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
|
16, sizeof(glm::vec4), &ovPush.tintColor);
|
||||||
|
vkCmdDraw(cmd, 6, 1, 0, 0);
|
||||||
|
|
||||||
|
descSlot++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Draw fog of war overlay over unexplored areas ---
|
||||||
|
if (hasServerMask && zone.areaID != 0 && overlayPipeline_ && fogDescSet_) {
|
||||||
|
bool hasAnyExplored = false;
|
||||||
|
for (int oi = 0; oi < static_cast<int>(zone.overlays.size()); oi++) {
|
||||||
|
if (exploredOverlays.count(oi) > 0) { hasAnyExplored = true; break; }
|
||||||
|
}
|
||||||
|
if (!hasAnyExplored && !zone.overlays.empty()) {
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline_);
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
|
overlayPipelineLayout_, 0, 1,
|
||||||
|
&fogDescSet_, 0, nullptr);
|
||||||
|
|
||||||
|
OverlayPush fogPush{};
|
||||||
|
fogPush.gridOffset = glm::vec2(0.0f, 0.0f);
|
||||||
|
fogPush.gridCols = 1.0f;
|
||||||
|
fogPush.gridRows = 1.0f;
|
||||||
|
fogPush.tintColor = glm::vec4(0.15f, 0.15f, 0.2f, 0.55f);
|
||||||
|
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||||
|
0, sizeof(WorldMapTilePush), &fogPush);
|
||||||
|
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
|
16, sizeof(glm::vec4), &fogPush.tintColor);
|
||||||
|
vkCmdDraw(cmd, 6, 1, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compositeTarget->endPass(cmd);
|
||||||
|
compositedIdx_ = zoneIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
228
src/rendering/world_map/coordinate_projection.cpp
Normal file
228
src/rendering/world_map/coordinate_projection.cpp
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
// coordinate_projection.cpp — Pure coordinate math for world map UV projection.
|
||||||
|
// Extracted from WorldMap::renderPosToMapUV, findBestContinentForPlayer,
|
||||||
|
// findZoneForPlayer, zoneBelongsToContinent, getContinentProjectionBounds,
|
||||||
|
// isRootContinent, isLeafContinent (Phase 2 of refactoring plan).
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
// ── Continent classification helpers ─────────────────────────
|
||||||
|
|
||||||
|
bool isRootContinent(const std::vector<Zone>& zones, int idx) {
|
||||||
|
if (idx < 0 || idx >= static_cast<int>(zones.size())) return false;
|
||||||
|
const auto& c = zones[idx];
|
||||||
|
if (c.areaID != 0 || c.wmaID == 0) return false;
|
||||||
|
for (const auto& z : zones) {
|
||||||
|
if (z.areaID == 0 && z.parentWorldMapID == c.wmaID) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isLeafContinent(const std::vector<Zone>& zones, int idx) {
|
||||||
|
if (idx < 0 || idx >= static_cast<int>(zones.size())) return false;
|
||||||
|
const auto& c = zones[idx];
|
||||||
|
if (c.areaID != 0) return false;
|
||||||
|
return c.parentWorldMapID != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UV projection ────────────────────────────────────────────
|
||||||
|
|
||||||
|
glm::vec2 renderPosToMapUV(const glm::vec3& renderPos,
|
||||||
|
const ZoneBounds& bounds,
|
||||||
|
bool isContinent) {
|
||||||
|
float wowX = renderPos.y;
|
||||||
|
float wowY = renderPos.x;
|
||||||
|
|
||||||
|
float denom_h = bounds.locLeft - bounds.locRight;
|
||||||
|
float denom_v = bounds.locTop - bounds.locBottom;
|
||||||
|
if (std::abs(denom_h) < 0.001f || std::abs(denom_v) < 0.001f)
|
||||||
|
return glm::vec2(0.5f, 0.5f);
|
||||||
|
|
||||||
|
float u = (bounds.locLeft - wowX) / denom_h;
|
||||||
|
float v = (bounds.locTop - wowY) / denom_v;
|
||||||
|
|
||||||
|
if (isContinent) {
|
||||||
|
constexpr float kVScale = 1.0f;
|
||||||
|
constexpr float kVOffset = -0.15f;
|
||||||
|
v = (v - 0.5f) * kVScale + 0.5f + kVOffset;
|
||||||
|
}
|
||||||
|
return glm::vec2(u, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Continent projection bounds ──────────────────────────────
|
||||||
|
|
||||||
|
bool getContinentProjectionBounds(const std::vector<Zone>& zones,
|
||||||
|
int contIdx,
|
||||||
|
float& left, float& right,
|
||||||
|
float& top, float& bottom) {
|
||||||
|
if (contIdx < 0 || contIdx >= static_cast<int>(zones.size())) return false;
|
||||||
|
const auto& cont = zones[contIdx];
|
||||||
|
if (cont.areaID != 0) return false;
|
||||||
|
|
||||||
|
if (std::abs(cont.bounds.locLeft - cont.bounds.locRight) > 0.001f &&
|
||||||
|
std::abs(cont.bounds.locTop - cont.bounds.locBottom) > 0.001f) {
|
||||||
|
left = cont.bounds.locLeft; right = cont.bounds.locRight;
|
||||||
|
top = cont.bounds.locTop; bottom = cont.bounds.locBottom;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> northEdges, southEdges, westEdges, eastEdges;
|
||||||
|
for (int zi = 0; zi < static_cast<int>(zones.size()); zi++) {
|
||||||
|
if (!zoneBelongsToContinent(zones, zi, contIdx)) continue;
|
||||||
|
const auto& z = zones[zi];
|
||||||
|
if (std::abs(z.bounds.locLeft - z.bounds.locRight) < 0.001f ||
|
||||||
|
std::abs(z.bounds.locTop - z.bounds.locBottom) < 0.001f) continue;
|
||||||
|
northEdges.push_back(std::max(z.bounds.locLeft, z.bounds.locRight));
|
||||||
|
southEdges.push_back(std::min(z.bounds.locLeft, z.bounds.locRight));
|
||||||
|
westEdges.push_back(std::max(z.bounds.locTop, z.bounds.locBottom));
|
||||||
|
eastEdges.push_back(std::min(z.bounds.locTop, z.bounds.locBottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (northEdges.size() < 3) {
|
||||||
|
left = cont.bounds.locLeft; right = cont.bounds.locRight;
|
||||||
|
top = cont.bounds.locTop; bottom = cont.bounds.locBottom;
|
||||||
|
return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f;
|
||||||
|
}
|
||||||
|
|
||||||
|
left = *std::max_element(northEdges.begin(), northEdges.end());
|
||||||
|
right = *std::min_element(southEdges.begin(), southEdges.end());
|
||||||
|
top = *std::max_element(westEdges.begin(), westEdges.end());
|
||||||
|
bottom = *std::min_element(eastEdges.begin(), eastEdges.end());
|
||||||
|
|
||||||
|
if (left <= right || top <= bottom) {
|
||||||
|
left = cont.bounds.locLeft; right = cont.bounds.locRight;
|
||||||
|
top = cont.bounds.locTop; bottom = cont.bounds.locBottom;
|
||||||
|
}
|
||||||
|
return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Player position lookups ──────────────────────────────────
|
||||||
|
|
||||||
|
int findBestContinentForPlayer(const std::vector<Zone>& zones,
|
||||||
|
const glm::vec3& playerRenderPos) {
|
||||||
|
float wowX = playerRenderPos.y;
|
||||||
|
float wowY = playerRenderPos.x;
|
||||||
|
|
||||||
|
int bestIdx = -1;
|
||||||
|
float bestArea = std::numeric_limits<float>::max();
|
||||||
|
float bestCenterDist2 = std::numeric_limits<float>::max();
|
||||||
|
|
||||||
|
bool hasLeaf = false;
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
if (zones[i].areaID == 0 && !isRootContinent(zones, i)) {
|
||||||
|
hasLeaf = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
const auto& z = zones[i];
|
||||||
|
if (z.areaID != 0) continue;
|
||||||
|
if (hasLeaf && isRootContinent(zones, i)) continue;
|
||||||
|
|
||||||
|
float minX = std::min(z.bounds.locLeft, z.bounds.locRight);
|
||||||
|
float maxX = std::max(z.bounds.locLeft, z.bounds.locRight);
|
||||||
|
float minY = std::min(z.bounds.locTop, z.bounds.locBottom);
|
||||||
|
float maxY = std::max(z.bounds.locTop, z.bounds.locBottom);
|
||||||
|
float spanX = maxX - minX;
|
||||||
|
float spanY = maxY - minY;
|
||||||
|
if (spanX < 0.001f || spanY < 0.001f) continue;
|
||||||
|
|
||||||
|
bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY);
|
||||||
|
float area = spanX * spanY;
|
||||||
|
if (contains) {
|
||||||
|
if (area < bestArea) { bestArea = area; bestIdx = i; }
|
||||||
|
} else if (bestIdx < 0) {
|
||||||
|
float cx = (minX + maxX) * 0.5f, cy = (minY + maxY) * 0.5f;
|
||||||
|
float dist2 = (wowX - cx) * (wowX - cx) + (wowY - cy) * (wowY - cy);
|
||||||
|
if (dist2 < bestCenterDist2) { bestCenterDist2 = dist2; bestIdx = i; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
int findZoneForPlayer(const std::vector<Zone>& zones,
|
||||||
|
const glm::vec3& playerRenderPos) {
|
||||||
|
float wowX = playerRenderPos.y;
|
||||||
|
float wowY = playerRenderPos.x;
|
||||||
|
|
||||||
|
int bestIdx = -1;
|
||||||
|
float bestArea = std::numeric_limits<float>::max();
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
const auto& z = zones[i];
|
||||||
|
if (z.areaID == 0) continue;
|
||||||
|
|
||||||
|
float minX = std::min(z.bounds.locLeft, z.bounds.locRight);
|
||||||
|
float maxX = std::max(z.bounds.locLeft, z.bounds.locRight);
|
||||||
|
float minY = std::min(z.bounds.locTop, z.bounds.locBottom);
|
||||||
|
float maxY = std::max(z.bounds.locTop, z.bounds.locBottom);
|
||||||
|
float spanX = maxX - minX, spanY = maxY - minY;
|
||||||
|
if (spanX < 0.001f || spanY < 0.001f) continue;
|
||||||
|
|
||||||
|
if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) {
|
||||||
|
float area = spanX * spanY;
|
||||||
|
if (area < bestArea) { bestArea = area; bestIdx = i; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Zone–continent relationship ──────────────────────────────
|
||||||
|
|
||||||
|
bool zoneBelongsToContinent(const std::vector<Zone>& zones,
|
||||||
|
int zoneIdx, int contIdx) {
|
||||||
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return false;
|
||||||
|
if (contIdx < 0 || contIdx >= static_cast<int>(zones.size())) return false;
|
||||||
|
|
||||||
|
const auto& z = zones[zoneIdx];
|
||||||
|
const auto& cont = zones[contIdx];
|
||||||
|
if (z.areaID == 0) return false;
|
||||||
|
|
||||||
|
// Prefer explicit parent link if available
|
||||||
|
if (z.parentWorldMapID != 0 && cont.wmaID != 0)
|
||||||
|
return z.parentWorldMapID == cont.wmaID;
|
||||||
|
|
||||||
|
// Fallback: spatial overlap heuristic
|
||||||
|
auto rectMinX = [](const Zone& a) { return std::min(a.bounds.locLeft, a.bounds.locRight); };
|
||||||
|
auto rectMaxX = [](const Zone& a) { return std::max(a.bounds.locLeft, a.bounds.locRight); };
|
||||||
|
auto rectMinY = [](const Zone& a) { return std::min(a.bounds.locTop, a.bounds.locBottom); };
|
||||||
|
auto rectMaxY = [](const Zone& a) { return std::max(a.bounds.locTop, a.bounds.locBottom); };
|
||||||
|
|
||||||
|
float zMinX = rectMinX(z), zMaxX = rectMaxX(z);
|
||||||
|
float zMinY = rectMinY(z), zMaxY = rectMaxY(z);
|
||||||
|
if ((zMaxX - zMinX) < 0.001f || (zMaxY - zMinY) < 0.001f) return false;
|
||||||
|
|
||||||
|
int bestContIdx = -1;
|
||||||
|
float bestOverlap = 0.0f;
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
const auto& c = zones[i];
|
||||||
|
if (c.areaID != 0) continue;
|
||||||
|
float cMinX = rectMinX(c), cMaxX = rectMaxX(c);
|
||||||
|
float cMinY = rectMinY(c), cMaxY = rectMaxY(c);
|
||||||
|
if ((cMaxX - cMinX) < 0.001f || (cMaxY - cMinY) < 0.001f) continue;
|
||||||
|
|
||||||
|
float ox = std::max(0.0f, std::min(zMaxX, cMaxX) - std::max(zMinX, cMinX));
|
||||||
|
float oy = std::max(0.0f, std::min(zMaxY, cMaxY) - std::max(zMinY, cMinY));
|
||||||
|
float overlap = ox * oy;
|
||||||
|
if (overlap > bestOverlap) { bestOverlap = overlap; bestContIdx = i; }
|
||||||
|
}
|
||||||
|
if (bestContIdx >= 0) return bestContIdx == contIdx;
|
||||||
|
|
||||||
|
float centerX = (z.bounds.locLeft + z.bounds.locRight) * 0.5f;
|
||||||
|
float centerY = (z.bounds.locTop + z.bounds.locBottom) * 0.5f;
|
||||||
|
return centerX >= rectMinX(cont) && centerX <= rectMaxX(cont) &&
|
||||||
|
centerY >= rectMinY(cont) && centerY <= rectMaxY(cont);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
485
src/rendering/world_map/data_repository.cpp
Normal file
485
src/rendering/world_map/data_repository.cpp
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
// data_repository.cpp — DBC data loading, ZMP pixel map, and zone/POI/overlay storage.
|
||||||
|
// Extracted from WorldMap::loadZonesFromDBC, loadPOIData, buildCosmicView
|
||||||
|
// (Phase 5 of refactoring plan).
|
||||||
|
#include "rendering/world_map/data_repository.hpp"
|
||||||
|
#include "rendering/world_map/map_resolver.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
#include "pipeline/dbc_layout.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include "core/application.hpp"
|
||||||
|
#include "game/expansion_profile.hpp"
|
||||||
|
#include "game/game_utils.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void DataRepository::clear() {
|
||||||
|
zones_.clear();
|
||||||
|
poiMarkers_.clear();
|
||||||
|
cosmicMaps_.clear();
|
||||||
|
azerothRegions_.clear();
|
||||||
|
exploreFlagByAreaId_.clear();
|
||||||
|
areaNameByAreaId_.clear();
|
||||||
|
areaIdToZoneIdx_.clear();
|
||||||
|
zmpZoneBounds_.clear();
|
||||||
|
zmpGrid_.fill(0);
|
||||||
|
zmpLoaded_ = false;
|
||||||
|
cosmicIdx_ = -1;
|
||||||
|
worldIdx_ = -1;
|
||||||
|
currentMapId_ = -1;
|
||||||
|
cosmicEnabled_ = true;
|
||||||
|
poisLoaded_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// DBC zone loading (moved from WorldMap::loadZonesFromDBC)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
void DataRepository::loadZones(const std::string& mapName,
|
||||||
|
pipeline::AssetManager& assetManager) {
|
||||||
|
if (!zones_.empty()) return;
|
||||||
|
|
||||||
|
const auto* activeLayout = pipeline::getActiveDBCLayout();
|
||||||
|
const auto* mapL = activeLayout ? activeLayout->getLayout("Map") : nullptr;
|
||||||
|
|
||||||
|
int mapID = -1;
|
||||||
|
auto mapDbc = assetManager.loadDBC("Map.dbc");
|
||||||
|
if (mapDbc && mapDbc->isLoaded()) {
|
||||||
|
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
|
||||||
|
std::string dir = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
|
||||||
|
if (dir == mapName) {
|
||||||
|
mapID = static_cast<int>(mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0));
|
||||||
|
LOG_INFO("DataRepository: Map.dbc '", mapName, "' -> mapID=", mapID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapID < 0) {
|
||||||
|
mapID = folderToMapId(mapName);
|
||||||
|
if (mapID < 0) {
|
||||||
|
LOG_WARNING("DataRepository: unknown map '", mapName, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use expansion-aware DBC layout when available; fall back to WotLK stock field
|
||||||
|
// indices (ID=0, ParentAreaNum=2, ExploreFlag=3) when layout metadata is missing.
|
||||||
|
const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr;
|
||||||
|
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||||
|
std::unordered_map<uint32_t, std::vector<uint32_t>> childBitsByParent;
|
||||||
|
auto areaDbc = assetManager.loadDBC("AreaTable.dbc");
|
||||||
|
// Bug fix: old code used > 3 which covers core fields (ID=0, ParentAreaNum=2,
|
||||||
|
// ExploreFlag=3). The > 11 threshold broke exploration for DBC variants with
|
||||||
|
// 4-11 fields. Load core exploration data with > 3; area name only when > 11.
|
||||||
|
if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) {
|
||||||
|
const uint32_t fieldCount = areaDbc->getFieldCount();
|
||||||
|
const uint32_t parentField = atL ? (*atL)["ParentAreaNum"] : 2;
|
||||||
|
for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) {
|
||||||
|
const uint32_t areaId = areaDbc->getUInt32(i, atL ? (*atL)["ID"] : 0);
|
||||||
|
const uint32_t exploreFlag = areaDbc->getUInt32(i, atL ? (*atL)["ExploreFlag"] : 3);
|
||||||
|
const uint32_t parentArea = areaDbc->getUInt32(i, parentField);
|
||||||
|
if (areaId != 0) exploreFlagByAreaId[areaId] = exploreFlag;
|
||||||
|
if (parentArea != 0) childBitsByParent[parentArea].push_back(exploreFlag);
|
||||||
|
// Cache area display name (field 11 = AreaName_lang enUS)
|
||||||
|
if (areaId != 0 && fieldCount > 11) {
|
||||||
|
std::string areaDispName = areaDbc->getString(i, 11);
|
||||||
|
if (!areaDispName.empty())
|
||||||
|
areaNameByAreaId_[areaId] = std::move(areaDispName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wmaDbc = assetManager.loadDBC("WorldMapArea.dbc");
|
||||||
|
if (!wmaDbc || !wmaDbc->isLoaded()) {
|
||||||
|
LOG_WARNING("DataRepository: WorldMapArea.dbc not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr;
|
||||||
|
|
||||||
|
int continentIdx = -1;
|
||||||
|
for (uint32_t i = 0; i < wmaDbc->getRecordCount(); i++) {
|
||||||
|
uint32_t recMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["MapID"] : 1);
|
||||||
|
if (static_cast<int>(recMapID) != mapID) continue;
|
||||||
|
|
||||||
|
Zone zone;
|
||||||
|
zone.wmaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ID"] : 0);
|
||||||
|
zone.areaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["AreaID"] : 2);
|
||||||
|
zone.areaName = wmaDbc->getString(i, wmaL ? (*wmaL)["AreaName"] : 3);
|
||||||
|
zone.bounds.locLeft = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocLeft"] : 4);
|
||||||
|
zone.bounds.locRight = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocRight"] : 5);
|
||||||
|
zone.bounds.locTop = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocTop"] : 6);
|
||||||
|
zone.bounds.locBottom = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocBottom"] : 7);
|
||||||
|
zone.displayMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["DisplayMapID"] : 8);
|
||||||
|
zone.parentWorldMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ParentWorldMapID"] : 10);
|
||||||
|
// Collect the zone's own AreaBit plus all subzone AreaBits
|
||||||
|
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
|
||||||
|
if (exploreIt != exploreFlagByAreaId.end())
|
||||||
|
zone.exploreBits.push_back(exploreIt->second);
|
||||||
|
auto childIt = childBitsByParent.find(zone.areaID);
|
||||||
|
if (childIt != childBitsByParent.end()) {
|
||||||
|
for (uint32_t bit : childIt->second)
|
||||||
|
zone.exploreBits.push_back(bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = static_cast<int>(zones_.size());
|
||||||
|
|
||||||
|
LOG_INFO("DataRepository: zone[", idx, "] areaID=", zone.areaID,
|
||||||
|
" '", zone.areaName, "' L=", zone.bounds.locLeft,
|
||||||
|
" R=", zone.bounds.locRight, " T=", zone.bounds.locTop,
|
||||||
|
" B=", zone.bounds.locBottom);
|
||||||
|
|
||||||
|
if (zone.areaID == 0 && continentIdx < 0)
|
||||||
|
continentIdx = idx;
|
||||||
|
|
||||||
|
zones_.push_back(std::move(zone));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive continent bounds from child zones if missing
|
||||||
|
for (int ci = 0; ci < static_cast<int>(zones_.size()); ci++) {
|
||||||
|
auto& cont = zones_[ci];
|
||||||
|
if (cont.areaID != 0) continue;
|
||||||
|
if (std::abs(cont.bounds.locLeft) > 0.001f || std::abs(cont.bounds.locRight) > 0.001f ||
|
||||||
|
std::abs(cont.bounds.locTop) > 0.001f || std::abs(cont.bounds.locBottom) > 0.001f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
for (const auto& z : zones_) {
|
||||||
|
if (z.areaID == 0) continue;
|
||||||
|
if (std::abs(z.bounds.locLeft - z.bounds.locRight) < 0.001f ||
|
||||||
|
std::abs(z.bounds.locTop - z.bounds.locBottom) < 0.001f)
|
||||||
|
continue;
|
||||||
|
if (z.parentWorldMapID != 0 && cont.wmaID != 0 && z.parentWorldMapID != cont.wmaID)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
cont.bounds.locLeft = z.bounds.locLeft; cont.bounds.locRight = z.bounds.locRight;
|
||||||
|
cont.bounds.locTop = z.bounds.locTop; cont.bounds.locBottom = z.bounds.locBottom;
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
cont.bounds.locLeft = std::min(cont.bounds.locLeft, z.bounds.locLeft);
|
||||||
|
cont.bounds.locRight = std::max(cont.bounds.locRight, z.bounds.locRight);
|
||||||
|
cont.bounds.locTop = std::min(cont.bounds.locTop, z.bounds.locTop);
|
||||||
|
cont.bounds.locBottom = std::max(cont.bounds.locBottom, z.bounds.locBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMapId_ = mapID;
|
||||||
|
exploreFlagByAreaId_ = exploreFlagByAreaId; // cache for overlay exploration checks
|
||||||
|
LOG_INFO("DataRepository: loaded ", zones_.size(), " zones for mapID=", mapID,
|
||||||
|
", continentIdx=", continentIdx);
|
||||||
|
|
||||||
|
// Build wmaID → zone index lookup
|
||||||
|
std::unordered_map<uint32_t, int> wmaIdToZoneIdx;
|
||||||
|
for (int i = 0; i < static_cast<int>(zones_.size()); i++)
|
||||||
|
wmaIdToZoneIdx[zones_[i].wmaID] = i;
|
||||||
|
|
||||||
|
// Parse WorldMapOverlay.dbc → attach overlay entries to their zones
|
||||||
|
auto wmoDbc = assetManager.loadDBC("WorldMapOverlay.dbc");
|
||||||
|
if (wmoDbc && wmoDbc->isLoaded()) {
|
||||||
|
// WotLK field layout:
|
||||||
|
// 0:ID, 1:WorldMapAreaID, 2-5:AreaTableID[4],
|
||||||
|
// 6:MapPointX, 7:MapPointY, 8:TextureName(str),
|
||||||
|
// 9:TextureWidth, 10:TextureHeight,
|
||||||
|
// 11:OffsetX, 12:OffsetY, 13-16:HitRect
|
||||||
|
int totalOverlays = 0;
|
||||||
|
for (uint32_t i = 0; i < wmoDbc->getRecordCount(); i++) {
|
||||||
|
uint32_t wmaID = wmoDbc->getUInt32(i, 1);
|
||||||
|
auto it = wmaIdToZoneIdx.find(wmaID);
|
||||||
|
if (it == wmaIdToZoneIdx.end()) continue;
|
||||||
|
|
||||||
|
OverlayEntry ov;
|
||||||
|
ov.areaIDs[0] = wmoDbc->getUInt32(i, 2);
|
||||||
|
ov.areaIDs[1] = wmoDbc->getUInt32(i, 3);
|
||||||
|
ov.areaIDs[2] = wmoDbc->getUInt32(i, 4);
|
||||||
|
ov.areaIDs[3] = wmoDbc->getUInt32(i, 5);
|
||||||
|
ov.textureName = wmoDbc->getString(i, 8);
|
||||||
|
ov.texWidth = static_cast<uint16_t>(wmoDbc->getUInt32(i, 9));
|
||||||
|
ov.texHeight = static_cast<uint16_t>(wmoDbc->getUInt32(i, 10));
|
||||||
|
ov.offsetX = static_cast<uint16_t>(wmoDbc->getUInt32(i, 11));
|
||||||
|
ov.offsetY = static_cast<uint16_t>(wmoDbc->getUInt32(i, 12));
|
||||||
|
// HitRect (fields 13-16): fast AABB pre-filter for subzone hover
|
||||||
|
ov.hitRectLeft = static_cast<uint16_t>(wmoDbc->getUInt32(i, 13));
|
||||||
|
ov.hitRectRight = static_cast<uint16_t>(wmoDbc->getUInt32(i, 14));
|
||||||
|
ov.hitRectTop = static_cast<uint16_t>(wmoDbc->getUInt32(i, 15));
|
||||||
|
ov.hitRectBottom = static_cast<uint16_t>(wmoDbc->getUInt32(i, 16));
|
||||||
|
|
||||||
|
if (ov.textureName.empty() || ov.texWidth == 0 || ov.texHeight == 0) continue;
|
||||||
|
|
||||||
|
ov.tileCols = (ov.texWidth + 255) / 256;
|
||||||
|
ov.tileRows = (ov.texHeight + 255) / 256;
|
||||||
|
|
||||||
|
zones_[it->second].overlays.push_back(std::move(ov));
|
||||||
|
totalOverlays++;
|
||||||
|
}
|
||||||
|
LOG_INFO("DataRepository: loaded ", totalOverlays, " overlay entries from WorldMapOverlay.dbc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a synthetic "Cosmic" zone for the cross-world map (Azeroth + Outland)
|
||||||
|
{
|
||||||
|
Zone cosmic;
|
||||||
|
cosmic.areaName = "Cosmic";
|
||||||
|
cosmicIdx_ = static_cast<int>(zones_.size());
|
||||||
|
zones_.push_back(std::move(cosmic));
|
||||||
|
LOG_INFO("DataRepository: added synthetic Cosmic zone at index ", cosmicIdx_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a synthetic "World" zone for the combined world overview map
|
||||||
|
{
|
||||||
|
Zone world;
|
||||||
|
world.areaName = "World";
|
||||||
|
worldIdx_ = static_cast<int>(zones_.size());
|
||||||
|
zones_.push_back(std::move(world));
|
||||||
|
LOG_INFO("DataRepository: added synthetic World zone at index ", worldIdx_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load area POI data (towns, dungeons, etc.)
|
||||||
|
loadPOIs(assetManager);
|
||||||
|
|
||||||
|
// Build areaID → zone index lookup for ZMP resolution
|
||||||
|
for (int i = 0; i < static_cast<int>(zones_.size()); i++) {
|
||||||
|
if (zones_[i].areaID != 0)
|
||||||
|
areaIdToZoneIdx_[zones_[i].areaID] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load ZMP pixel map for continent-level hover detection
|
||||||
|
loadZmpPixelMap(mapName, assetManager);
|
||||||
|
|
||||||
|
// Build views based on active expansion
|
||||||
|
buildCosmicView();
|
||||||
|
buildAzerothView();
|
||||||
|
}
|
||||||
|
|
||||||
|
int DataRepository::getExpansionLevel() {
|
||||||
|
if (game::isClassicLikeExpansion()) return 0;
|
||||||
|
if (game::isActiveExpansion("tbc")) return 1;
|
||||||
|
return 2; // WotLK and above
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// ZMP pixel map loading
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
void DataRepository::loadZmpPixelMap(const std::string& continentName,
|
||||||
|
pipeline::AssetManager& assetManager) {
|
||||||
|
zmpGrid_.fill(0);
|
||||||
|
zmpLoaded_ = false;
|
||||||
|
|
||||||
|
// ZMP path: Interface\WorldMap\{name_lower}.zmp
|
||||||
|
std::string lower = continentName;
|
||||||
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
std::string zmpPath = "Interface\\WorldMap\\" + lower + ".zmp";
|
||||||
|
|
||||||
|
auto data = assetManager.readFileOptional(zmpPath);
|
||||||
|
if (data.empty()) {
|
||||||
|
LOG_INFO("DataRepository: ZMP not found at '", zmpPath, "' (ok for non-continent maps)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZMP is a 128x128 grid of uint32 = 65536 bytes
|
||||||
|
constexpr size_t kExpectedSize = ZMP_SIZE * ZMP_SIZE * sizeof(uint32_t);
|
||||||
|
if (data.size() != kExpectedSize) {
|
||||||
|
LOG_WARNING("DataRepository: ZMP '", zmpPath, "' unexpected size ",
|
||||||
|
data.size(), " (expected ", kExpectedSize, ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(zmpGrid_.data(), data.data(), kExpectedSize);
|
||||||
|
zmpLoaded_ = true;
|
||||||
|
|
||||||
|
// Count non-zero cells and find grid extent for diagnostic
|
||||||
|
int nonZero = 0;
|
||||||
|
int maxRow = -1, maxCol = -1;
|
||||||
|
for (int r = 0; r < ZMP_SIZE; r++) {
|
||||||
|
for (int c = 0; c < ZMP_SIZE; c++) {
|
||||||
|
if (zmpGrid_[r * ZMP_SIZE + c] != 0) {
|
||||||
|
nonZero++;
|
||||||
|
if (r > maxRow) maxRow = r;
|
||||||
|
if (c > maxCol) maxCol = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_INFO("DataRepository: loaded ZMP '", zmpPath, "' — ",
|
||||||
|
nonZero, "/", ZMP_SIZE * ZMP_SIZE, " non-zero cells, "
|
||||||
|
"maxCol=", maxCol, " maxRow=", maxRow,
|
||||||
|
" (if ~125/~111 → maps to 1024x768 FBO, if ~127/~127 → maps to 1002x668 visible)");
|
||||||
|
|
||||||
|
// Derive zone bounding boxes from ZMP grid
|
||||||
|
buildZmpZoneBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataRepository::buildZmpZoneBounds() {
|
||||||
|
zmpZoneBounds_.clear();
|
||||||
|
if (!zmpLoaded_) return;
|
||||||
|
|
||||||
|
// Scan the 128x128 ZMP grid and find the bounding box of each area ID's pixels.
|
||||||
|
// The ZMP grid maps directly to the visible 1002×668 content area, so
|
||||||
|
// (col/128, row/128) gives display UV without FBO conversion.
|
||||||
|
struct RawRect { int minCol = ZMP_SIZE, maxCol = -1, minRow = ZMP_SIZE, maxRow = -1; };
|
||||||
|
std::unordered_map<uint32_t, RawRect> areaRects;
|
||||||
|
|
||||||
|
for (int row = 0; row < ZMP_SIZE; row++) {
|
||||||
|
for (int col = 0; col < ZMP_SIZE; col++) {
|
||||||
|
uint32_t areaId = zmpGrid_[row * ZMP_SIZE + col];
|
||||||
|
if (areaId == 0) continue;
|
||||||
|
auto& r = areaRects[areaId];
|
||||||
|
r.minCol = std::min(r.minCol, col);
|
||||||
|
r.maxCol = std::max(r.maxCol, col);
|
||||||
|
r.minRow = std::min(r.minRow, row);
|
||||||
|
r.maxRow = std::max(r.maxRow, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map area ID bounding boxes → zone index bounding boxes.
|
||||||
|
// Multiple area IDs may resolve to the same zone, so union their rects.
|
||||||
|
constexpr float kInvSize = 1.0f / static_cast<float>(ZMP_SIZE);
|
||||||
|
int mapped = 0;
|
||||||
|
for (const auto& [areaId, rect] : areaRects) {
|
||||||
|
int zi = zoneIndexForAreaId(areaId);
|
||||||
|
if (zi < 0) continue;
|
||||||
|
|
||||||
|
float uMin = static_cast<float>(rect.minCol) * kInvSize;
|
||||||
|
float uMax = static_cast<float>(rect.maxCol + 1) * kInvSize;
|
||||||
|
float vMin = static_cast<float>(rect.minRow) * kInvSize;
|
||||||
|
float vMax = static_cast<float>(rect.maxRow + 1) * kInvSize;
|
||||||
|
|
||||||
|
auto it = zmpZoneBounds_.find(zi);
|
||||||
|
if (it != zmpZoneBounds_.end()) {
|
||||||
|
// Union with existing rect for this zone
|
||||||
|
it->second.uMin = std::min(it->second.uMin, uMin);
|
||||||
|
it->second.uMax = std::max(it->second.uMax, uMax);
|
||||||
|
it->second.vMin = std::min(it->second.vMin, vMin);
|
||||||
|
it->second.vMax = std::max(it->second.vMax, vMax);
|
||||||
|
} else {
|
||||||
|
ZmpRect zr;
|
||||||
|
zr.uMin = uMin; zr.uMax = uMax;
|
||||||
|
zr.vMin = vMin; zr.vMax = vMax;
|
||||||
|
zr.valid = true;
|
||||||
|
zmpZoneBounds_[zi] = zr;
|
||||||
|
mapped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("DataRepository: built ZMP zone bounds for ", mapped, " zones from ",
|
||||||
|
areaRects.size(), " area IDs");
|
||||||
|
}
|
||||||
|
|
||||||
|
int DataRepository::zoneIndexForAreaId(uint32_t areaId) const {
|
||||||
|
if (areaId == 0) return -1;
|
||||||
|
auto it = areaIdToZoneIdx_.find(areaId);
|
||||||
|
if (it != areaIdToZoneIdx_.end()) return it->second;
|
||||||
|
|
||||||
|
// Fallback: check if areaId is a sub-zone whose parent is in our zone list.
|
||||||
|
// Some ZMP cells reference sub-area IDs not directly in WorldMapArea.dbc.
|
||||||
|
// Walk the AreaTable parent chain via exploreFlagByAreaId_ (which was built
|
||||||
|
// from AreaTable.dbc and includes parentArea relationships).
|
||||||
|
// For now, iterate zones looking for one whose overlays reference this areaId.
|
||||||
|
for (int i = 0; i < static_cast<int>(zones_.size()); i++) {
|
||||||
|
for (const auto& ov : zones_[i].overlays) {
|
||||||
|
for (int j = 0; j < 4; j++) {
|
||||||
|
if (ov.areaIDs[j] == areaId) return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataRepository::loadPOIs(pipeline::AssetManager& assetManager) {
|
||||||
|
if (poisLoaded_) return;
|
||||||
|
poisLoaded_ = true;
|
||||||
|
|
||||||
|
auto poiDbc = assetManager.loadDBC("AreaPOI.dbc");
|
||||||
|
if (!poiDbc || !poiDbc->isLoaded()) {
|
||||||
|
LOG_INFO("DataRepository: AreaPOI.dbc not found, skipping POI markers");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t fieldCount = poiDbc->getFieldCount();
|
||||||
|
if (fieldCount < 17) {
|
||||||
|
LOG_WARNING("DataRepository: AreaPOI.dbc has too few fields (", fieldCount, ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreaPOI.dbc field layout (WotLK 3.3.5a):
|
||||||
|
// 0:ID, 1:Importance, 2-10:Icon[9], 11:FactionID,
|
||||||
|
// 12:X, 13:Y, 14:Z, 15:MapID,
|
||||||
|
// 16:Name_lang (enUS), ...
|
||||||
|
int loaded = 0;
|
||||||
|
for (uint32_t i = 0; i < poiDbc->getRecordCount(); i++) {
|
||||||
|
POI poi;
|
||||||
|
poi.id = poiDbc->getUInt32(i, 0);
|
||||||
|
poi.importance = poiDbc->getUInt32(i, 1);
|
||||||
|
poi.iconType = poiDbc->getUInt32(i, 2);
|
||||||
|
poi.factionId = poiDbc->getUInt32(i, 11);
|
||||||
|
poi.wowX = poiDbc->getFloat(i, 12);
|
||||||
|
poi.wowY = poiDbc->getFloat(i, 13);
|
||||||
|
poi.wowZ = poiDbc->getFloat(i, 14);
|
||||||
|
poi.mapId = poiDbc->getUInt32(i, 15);
|
||||||
|
poi.name = poiDbc->getString(i, 16);
|
||||||
|
|
||||||
|
if (poi.name.empty()) continue;
|
||||||
|
|
||||||
|
poiMarkers_.push_back(std::move(poi));
|
||||||
|
loaded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by importance ascending so high-importance POIs are drawn last (on top)
|
||||||
|
std::sort(poiMarkers_.begin(), poiMarkers_.end(),
|
||||||
|
[](const POI& a, const POI& b) { return a.importance < b.importance; });
|
||||||
|
|
||||||
|
LOG_INFO("DataRepository: loaded ", loaded, " POI markers from AreaPOI.dbc");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataRepository::buildCosmicView(int /*expLevel*/) {
|
||||||
|
cosmicMaps_.clear();
|
||||||
|
|
||||||
|
if (game::isClassicLikeExpansion()) {
|
||||||
|
// Vanilla/Classic: No cosmic view — skip from WORLD straight to CONTINENT.
|
||||||
|
cosmicEnabled_ = false;
|
||||||
|
LOG_INFO("DataRepository: Classic mode — cosmic view disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cosmicEnabled_ = true;
|
||||||
|
|
||||||
|
// Azeroth (EK + Kalimdor) — always present; bottom-right region of cosmic map
|
||||||
|
cosmicMaps_.push_back({0, "Azeroth", 0.58f, 0.05f, 0.95f, 0.95f});
|
||||||
|
|
||||||
|
if (game::isActiveExpansion("tbc") || game::isActiveExpansion("wotlk")) {
|
||||||
|
// TBC+: Add Outland — top-left region of cosmic map
|
||||||
|
cosmicMaps_.push_back({530, "Outland", 0.05f, 0.10f, 0.55f, 0.90f});
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("DataRepository: cosmic view built with ", cosmicMaps_.size(), " landmasses");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataRepository::buildAzerothView(int /*expLevel*/) {
|
||||||
|
azerothRegions_.clear();
|
||||||
|
|
||||||
|
// Clickable continent regions on the Azeroth world map (azeroth1-12.blp).
|
||||||
|
// UV coordinates are approximate positions of each landmass on the combined map.
|
||||||
|
|
||||||
|
// Eastern Kingdoms — right side of the Azeroth map
|
||||||
|
azerothRegions_.push_back({0, mapDisplayName(0), 0.55f, 0.05f, 0.95f, 0.95f});
|
||||||
|
|
||||||
|
// Kalimdor — left side of the Azeroth map
|
||||||
|
azerothRegions_.push_back({1, mapDisplayName(1), 0.05f, 0.10f, 0.45f, 0.95f});
|
||||||
|
|
||||||
|
if (game::isActiveExpansion("wotlk")) {
|
||||||
|
// WotLK: Northrend — top-center of the Azeroth map
|
||||||
|
azerothRegions_.push_back({571, mapDisplayName(571), 0.30f, 0.0f, 0.72f, 0.28f});
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("DataRepository: Azeroth view built with ", azerothRegions_.size(), " continent regions");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
119
src/rendering/world_map/exploration_state.cpp
Normal file
119
src/rendering/world_map/exploration_state.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// exploration_state.cpp — Fog of war / exploration tracking implementation.
|
||||||
|
// Extracted from WorldMap::updateExploration, setServerExplorationMask
|
||||||
|
// (Phase 3 of refactoring plan).
|
||||||
|
#include "rendering/world_map/exploration_state.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void ExplorationState::setServerMask(const std::vector<uint32_t>& masks, bool hasData) {
|
||||||
|
if (!hasData || masks.empty()) {
|
||||||
|
// New session or no data yet — reset both server mask and local accumulation
|
||||||
|
if (hasServerMask_) {
|
||||||
|
locallyExploredZones_.clear();
|
||||||
|
}
|
||||||
|
hasServerMask_ = false;
|
||||||
|
serverMask_.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasServerMask_ = true;
|
||||||
|
serverMask_ = masks;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExplorationState::isBitSet(uint32_t bitIndex) const {
|
||||||
|
if (!hasServerMask_ || serverMask_.empty()) return false;
|
||||||
|
const size_t word = bitIndex / 32;
|
||||||
|
if (word >= serverMask_.size()) return false;
|
||||||
|
return (serverMask_[word] & (1u << (bitIndex % 32))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExplorationState::update(const std::vector<Zone>& zones,
|
||||||
|
const glm::vec3& playerRenderPos,
|
||||||
|
int currentZoneIdx,
|
||||||
|
const std::unordered_map<uint32_t, uint32_t>& exploreFlagByAreaId) {
|
||||||
|
overlaysChanged_ = false;
|
||||||
|
|
||||||
|
if (hasServerMask_) {
|
||||||
|
exploredZones_.clear();
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
const auto& z = zones[i];
|
||||||
|
if (z.areaID == 0 || z.exploreBits.empty()) continue;
|
||||||
|
for (uint32_t bit : z.exploreBits) {
|
||||||
|
if (isBitSet(bit)) {
|
||||||
|
exploredZones_.insert(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also reveal the zone the player is currently standing in so the map isn't
|
||||||
|
// pitch-black the moment they first enter a new zone.
|
||||||
|
int curZone = findZoneForPlayer(zones, playerRenderPos);
|
||||||
|
if (curZone >= 0) exploredZones_.insert(curZone);
|
||||||
|
|
||||||
|
// Per-overlay exploration: check each overlay's areaIDs against the exploration mask
|
||||||
|
std::unordered_set<int> newExploredOverlays;
|
||||||
|
if (currentZoneIdx >= 0 && currentZoneIdx < static_cast<int>(zones.size())) {
|
||||||
|
const auto& curZoneData = zones[currentZoneIdx];
|
||||||
|
for (int oi = 0; oi < static_cast<int>(curZoneData.overlays.size()); oi++) {
|
||||||
|
const auto& ov = curZoneData.overlays[oi];
|
||||||
|
bool revealed = false;
|
||||||
|
for (int a = 0; a < 4; a++) {
|
||||||
|
if (ov.areaIDs[a] == 0) continue;
|
||||||
|
auto flagIt = exploreFlagByAreaId.find(ov.areaIDs[a]);
|
||||||
|
if (flagIt != exploreFlagByAreaId.end() && isBitSet(flagIt->second)) {
|
||||||
|
revealed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (revealed) newExploredOverlays.insert(oi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newExploredOverlays != exploredOverlays_) {
|
||||||
|
exploredOverlays_ = std::move(newExploredOverlays);
|
||||||
|
overlaysChanged_ = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server mask unavailable — fall back to locally-accumulated position tracking.
|
||||||
|
float wowX = playerRenderPos.y;
|
||||||
|
float wowY = playerRenderPos.x;
|
||||||
|
|
||||||
|
bool foundPos = false;
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
const auto& z = zones[i];
|
||||||
|
if (z.areaID == 0) continue;
|
||||||
|
float minX = std::min(z.bounds.locLeft, z.bounds.locRight);
|
||||||
|
float maxX = std::max(z.bounds.locLeft, z.bounds.locRight);
|
||||||
|
float minY = std::min(z.bounds.locTop, z.bounds.locBottom);
|
||||||
|
float maxY = std::max(z.bounds.locTop, z.bounds.locBottom);
|
||||||
|
if (maxX - minX < 0.001f || maxY - minY < 0.001f) continue;
|
||||||
|
if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) {
|
||||||
|
locallyExploredZones_.insert(i);
|
||||||
|
foundPos = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundPos) {
|
||||||
|
int zoneIdx = findZoneForPlayer(zones, playerRenderPos);
|
||||||
|
if (zoneIdx >= 0) locallyExploredZones_.insert(zoneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the accumulated local set
|
||||||
|
exploredZones_ = locallyExploredZones_;
|
||||||
|
|
||||||
|
// Without server mask, mark all overlays as explored (no fog of war)
|
||||||
|
exploredOverlays_.clear();
|
||||||
|
if (currentZoneIdx >= 0 && currentZoneIdx < static_cast<int>(zones.size())) {
|
||||||
|
for (int oi = 0; oi < static_cast<int>(zones[currentZoneIdx].overlays.size()); oi++)
|
||||||
|
exploredOverlays_.insert(oi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
57
src/rendering/world_map/input_handler.cpp
Normal file
57
src/rendering/world_map/input_handler.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// input_handler.cpp — Input processing for the world map.
|
||||||
|
// Extracted from WorldMap::render (Phase 9 of refactoring plan).
|
||||||
|
#include "rendering/world_map/input_handler.hpp"
|
||||||
|
#include "core/input.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
InputResult InputHandler::process(ViewLevel currentLevel,
|
||||||
|
int hoveredZoneIdx,
|
||||||
|
bool cosmicEnabled) {
|
||||||
|
InputResult result;
|
||||||
|
auto& input = core::Input::getInstance();
|
||||||
|
|
||||||
|
// ESC closes the map
|
||||||
|
if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
|
||||||
|
result.action = InputAction::CLOSE;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll wheel zoom
|
||||||
|
auto& io = ImGui::GetIO();
|
||||||
|
float wheelDelta = io.MouseWheel;
|
||||||
|
if (std::abs(wheelDelta) < 0.001f)
|
||||||
|
wheelDelta = input.getMouseWheelDelta();
|
||||||
|
|
||||||
|
if (wheelDelta > 0.0f) {
|
||||||
|
result.action = InputAction::ZOOM_IN;
|
||||||
|
return result;
|
||||||
|
} else if (wheelDelta < 0.0f) {
|
||||||
|
result.action = InputAction::ZOOM_OUT;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continent view: left-click on hovered zone (from previous frame)
|
||||||
|
if (currentLevel == ViewLevel::CONTINENT && hoveredZoneIdx >= 0 &&
|
||||||
|
input.isMouseButtonJustPressed(1)) {
|
||||||
|
result.action = InputAction::CLICK_ZONE;
|
||||||
|
result.targetIdx = hoveredZoneIdx;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-click to go back (zone → continent; continent → world)
|
||||||
|
if (io.MouseClicked[1]) {
|
||||||
|
result.action = InputAction::RIGHT_CLICK_BACK;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
53
src/rendering/world_map/layers/coordinate_display.cpp
Normal file
53
src/rendering/world_map/layers/coordinate_display.cpp
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// coordinate_display.cpp — WoW coordinates under cursor on the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/coordinate_display.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void CoordinateDisplay::render(const LayerContext& ctx) {
|
||||||
|
if (ctx.currentZoneIdx < 0) return;
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
auto& io = ImGui::GetIO();
|
||||||
|
ImVec2 mp = io.MousePos;
|
||||||
|
if (mp.x < ctx.imgMin.x || mp.x > ctx.imgMin.x + ctx.displayW ||
|
||||||
|
mp.y < ctx.imgMin.y || mp.y > ctx.imgMin.y + ctx.displayH)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float mu = (mp.x - ctx.imgMin.x) / ctx.displayW;
|
||||||
|
float mv = (mp.y - ctx.imgMin.y) / ctx.displayH;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
float left = zone.bounds.locLeft, right = zone.bounds.locRight;
|
||||||
|
float top = zone.bounds.locTop, bottom = zone.bounds.locBottom;
|
||||||
|
if (zone.areaID == 0) {
|
||||||
|
float l, r, t, b;
|
||||||
|
getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b);
|
||||||
|
left = l; right = r; top = t; bottom = b;
|
||||||
|
// Undo the kVOffset applied during renderPosToMapUV for continent
|
||||||
|
constexpr float kVOffset = -0.15f;
|
||||||
|
mv -= kVOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
float hWowX = left - mu * (left - right);
|
||||||
|
float hWowY = top - mv * (top - bottom);
|
||||||
|
|
||||||
|
char coordBuf[32];
|
||||||
|
snprintf(coordBuf, sizeof(coordBuf), "%.0f, %.0f", hWowX, hWowY);
|
||||||
|
ImVec2 coordSz = ImGui::CalcTextSize(coordBuf);
|
||||||
|
float cx = ctx.imgMin.x + ctx.displayW - coordSz.x - 8.0f;
|
||||||
|
float cy = ctx.imgMin.y + ctx.displayH - coordSz.y - 8.0f;
|
||||||
|
ctx.drawList->AddText(ImVec2(cx + 1.0f, cy + 1.0f), IM_COL32(0, 0, 0, 180), coordBuf);
|
||||||
|
ctx.drawList->AddText(ImVec2(cx, cy), IM_COL32(220, 210, 150, 230), coordBuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
54
src/rendering/world_map/layers/corpse_marker_layer.cpp
Normal file
54
src/rendering/world_map/layers/corpse_marker_layer.cpp
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// corpse_marker_layer.cpp — Death corpse X marker on the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/corpse_marker_layer.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void CorpseMarkerLayer::render(const LayerContext& ctx) {
|
||||||
|
if (!hasCorpse_) return;
|
||||||
|
if (ctx.currentZoneIdx < 0) return;
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
ZoneBounds bounds = zone.bounds;
|
||||||
|
bool isContinent = zone.areaID == 0;
|
||||||
|
if (isContinent) {
|
||||||
|
float l, r, t, b;
|
||||||
|
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||||
|
bounds = {l, r, t, b};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec2 uv = renderPosToMapUV(corpseRenderPos_, bounds, isContinent);
|
||||||
|
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) return;
|
||||||
|
|
||||||
|
float cx = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||||
|
float cy = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||||
|
constexpr float R = 5.0f;
|
||||||
|
constexpr float T = 1.8f;
|
||||||
|
// Dark outline
|
||||||
|
ctx.drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R),
|
||||||
|
IM_COL32(0, 0, 0, 220), T + 1.5f);
|
||||||
|
ctx.drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R),
|
||||||
|
IM_COL32(0, 0, 0, 220), T + 1.5f);
|
||||||
|
// Bone-white X
|
||||||
|
ctx.drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R),
|
||||||
|
IM_COL32(230, 220, 200, 240), T);
|
||||||
|
ctx.drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R),
|
||||||
|
IM_COL32(230, 220, 200, 240), T);
|
||||||
|
// Tooltip on hover
|
||||||
|
ImVec2 mp = ImGui::GetMousePos();
|
||||||
|
float dx = mp.x - cx, dy = mp.y - cy;
|
||||||
|
if (dx * dx + dy * dy < 64.0f) {
|
||||||
|
ImGui::SetTooltip("Your corpse");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
52
src/rendering/world_map/layers/party_dot_layer.cpp
Normal file
52
src/rendering/world_map/layers/party_dot_layer.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// party_dot_layer.cpp — Party member position dots on the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/party_dot_layer.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void PartyDotLayer::render(const LayerContext& ctx) {
|
||||||
|
if (!dots_ || dots_->empty()) return;
|
||||||
|
if (ctx.currentZoneIdx < 0) return;
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
ZoneBounds bounds = zone.bounds;
|
||||||
|
bool isContinent = zone.areaID == 0;
|
||||||
|
if (isContinent) {
|
||||||
|
float l, r, t, b;
|
||||||
|
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||||
|
bounds = {l, r, t, b};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
for (const auto& dot : *dots_) {
|
||||||
|
glm::vec2 uv = renderPosToMapUV(dot.renderPos, bounds, isContinent);
|
||||||
|
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||||
|
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||||
|
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||||
|
ctx.drawList->AddCircleFilled(ImVec2(px, py), 5.0f, dot.color);
|
||||||
|
ctx.drawList->AddCircle(ImVec2(px, py), 5.0f, IM_COL32(0, 0, 0, 200), 0, 1.5f);
|
||||||
|
if (!dot.name.empty()) {
|
||||||
|
ImVec2 mp = ImGui::GetMousePos();
|
||||||
|
float dx = mp.x - px, dy = mp.y - py;
|
||||||
|
if (dx * dx + dy * dy <= 49.0f) {
|
||||||
|
ImGui::SetTooltip("%s", dot.name.c_str());
|
||||||
|
}
|
||||||
|
ImVec2 nameSz = font->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f, dot.name.c_str());
|
||||||
|
float tx = px - nameSz.x * 0.5f;
|
||||||
|
float ty = py - nameSz.y - 7.0f;
|
||||||
|
ctx.drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), dot.name.c_str());
|
||||||
|
ctx.drawList->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 220), dot.name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
57
src/rendering/world_map/layers/player_marker_layer.cpp
Normal file
57
src/rendering/world_map/layers/player_marker_layer.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// player_marker_layer.cpp — Directional player arrow on the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/player_marker_layer.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void PlayerMarkerLayer::render(const LayerContext& ctx) {
|
||||||
|
if (ctx.currentZoneIdx < 0) return;
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
ZoneBounds bounds = zone.bounds;
|
||||||
|
bool isContinent = zone.areaID == 0;
|
||||||
|
|
||||||
|
// In continent view, only show the player marker if they are actually
|
||||||
|
// in a zone belonging to this continent (don't bleed across continents).
|
||||||
|
if (isContinent) {
|
||||||
|
int playerZone = findZoneForPlayer(*ctx.zones, ctx.playerRenderPos);
|
||||||
|
if (playerZone < 0 || !zoneBelongsToContinent(*ctx.zones, playerZone, ctx.currentZoneIdx))
|
||||||
|
return;
|
||||||
|
float l, r, t, b;
|
||||||
|
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||||
|
bounds = {l, r, t, b};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec2 playerUV = renderPosToMapUV(ctx.playerRenderPos, bounds, isContinent);
|
||||||
|
if (playerUV.x < 0.0f || playerUV.x > 1.0f ||
|
||||||
|
playerUV.y < 0.0f || playerUV.y > 1.0f) return;
|
||||||
|
|
||||||
|
float px = ctx.imgMin.x + playerUV.x * ctx.displayW;
|
||||||
|
float py = ctx.imgMin.y + playerUV.y * ctx.displayH;
|
||||||
|
|
||||||
|
// Directional arrow: render-space (cos,sin) maps to screen (-dx,-dy)
|
||||||
|
float yawRad = glm::radians(ctx.playerYawDeg);
|
||||||
|
float adx = -std::cos(yawRad);
|
||||||
|
float ady = -std::sin(yawRad);
|
||||||
|
float apx = -ady, apy = adx;
|
||||||
|
constexpr float TIP = 9.0f;
|
||||||
|
constexpr float TAIL = 4.0f;
|
||||||
|
constexpr float HALF = 5.0f;
|
||||||
|
ImVec2 tip(px + adx * TIP, py + ady * TIP);
|
||||||
|
ImVec2 bl (px - adx * TAIL + apx * HALF, py - ady * TAIL + apy * HALF);
|
||||||
|
ImVec2 br (px - adx * TAIL - apx * HALF, py - ady * TAIL - apy * HALF);
|
||||||
|
ctx.drawList->AddTriangleFilled(tip, bl, br, IM_COL32(255, 40, 40, 255));
|
||||||
|
ctx.drawList->AddTriangle(tip, bl, br, IM_COL32(0, 0, 0, 200), 1.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
102
src/rendering/world_map/layers/poi_marker_layer.cpp
Normal file
102
src/rendering/world_map/layers/poi_marker_layer.cpp
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// poi_marker_layer.cpp — Town/dungeon/capital POI icons on the world map.
|
||||||
|
// Extracted from WorldMap::renderPOIMarkers (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/poi_marker_layer.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include "core/coordinates.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void POIMarkerLayer::render(const LayerContext& ctx) {
|
||||||
|
if (!markers_ || markers_->empty()) return;
|
||||||
|
if (ctx.currentZoneIdx < 0) return;
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
ZoneBounds bounds = zone.bounds;
|
||||||
|
bool isContinent = zone.areaID == 0;
|
||||||
|
if (isContinent) {
|
||||||
|
float l, r, t, b;
|
||||||
|
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||||
|
bounds = {l, r, t, b};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 mp = ImGui::GetMousePos();
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
|
||||||
|
for (const auto& poi : *markers_) {
|
||||||
|
if (static_cast<int>(poi.mapId) != ctx.currentMapId) continue;
|
||||||
|
|
||||||
|
glm::vec3 rPos = core::coords::canonicalToRender(
|
||||||
|
glm::vec3(poi.wowX, poi.wowY, poi.wowZ));
|
||||||
|
glm::vec2 uv = renderPosToMapUV(rPos, bounds, isContinent);
|
||||||
|
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||||
|
|
||||||
|
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||||
|
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||||
|
|
||||||
|
float iconSize = (poi.importance >= 2) ? 7.0f :
|
||||||
|
(poi.importance >= 1) ? 5.0f : 3.0f;
|
||||||
|
|
||||||
|
ImU32 fillColor, borderColor;
|
||||||
|
if (poi.factionId == 469) {
|
||||||
|
fillColor = IM_COL32(60, 120, 255, 200);
|
||||||
|
borderColor = IM_COL32(20, 60, 180, 220);
|
||||||
|
} else if (poi.factionId == 67) {
|
||||||
|
fillColor = IM_COL32(255, 60, 60, 200);
|
||||||
|
borderColor = IM_COL32(180, 20, 20, 220);
|
||||||
|
} else {
|
||||||
|
fillColor = IM_COL32(255, 215, 0, 200);
|
||||||
|
borderColor = IM_COL32(180, 150, 0, 220);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poi.importance >= 2) {
|
||||||
|
ctx.drawList->AddCircleFilled(ImVec2(px, py), iconSize + 2.0f,
|
||||||
|
IM_COL32(255, 255, 200, 30));
|
||||||
|
ctx.drawList->AddCircleFilled(ImVec2(px, py), iconSize, fillColor);
|
||||||
|
ctx.drawList->AddCircle(ImVec2(px, py), iconSize, borderColor, 0, 2.0f);
|
||||||
|
} else if (poi.importance >= 1) {
|
||||||
|
float H = iconSize;
|
||||||
|
ImVec2 top2(px, py - H);
|
||||||
|
ImVec2 right2(px + H, py );
|
||||||
|
ImVec2 bot2(px, py + H);
|
||||||
|
ImVec2 left2(px - H, py );
|
||||||
|
ctx.drawList->AddQuadFilled(top2, right2, bot2, left2, fillColor);
|
||||||
|
ctx.drawList->AddQuad(top2, right2, bot2, left2, borderColor, 1.2f);
|
||||||
|
} else {
|
||||||
|
ctx.drawList->AddCircleFilled(ImVec2(px, py), iconSize, fillColor);
|
||||||
|
ctx.drawList->AddCircle(ImVec2(px, py), iconSize, borderColor, 0, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poi.importance >= 1 && ctx.viewLevel == ViewLevel::ZONE && !poi.name.empty()) {
|
||||||
|
float fontSize = (poi.importance >= 2) ? ImGui::GetFontSize() * 0.85f :
|
||||||
|
ImGui::GetFontSize() * 0.75f;
|
||||||
|
ImVec2 nameSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, poi.name.c_str());
|
||||||
|
float tx = px - nameSz.x * 0.5f;
|
||||||
|
float ty = py + iconSize + 2.0f;
|
||||||
|
ctx.drawList->AddText(font, fontSize,
|
||||||
|
ImVec2(tx + 1.0f, ty + 1.0f),
|
||||||
|
IM_COL32(0, 0, 0, 180), poi.name.c_str());
|
||||||
|
ctx.drawList->AddText(font, fontSize,
|
||||||
|
ImVec2(tx, ty), IM_COL32(255, 255, 255, 210),
|
||||||
|
poi.name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
float dx = mp.x - px, dy = mp.y - py;
|
||||||
|
float hitRadius = iconSize + 4.0f;
|
||||||
|
if (dx * dx + dy * dy < hitRadius * hitRadius && !poi.name.empty()) {
|
||||||
|
if (!poi.description.empty())
|
||||||
|
ImGui::SetTooltip("%s\n%s", poi.name.c_str(), poi.description.c_str());
|
||||||
|
else
|
||||||
|
ImGui::SetTooltip("%s", poi.name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
60
src/rendering/world_map/layers/quest_poi_layer.cpp
Normal file
60
src/rendering/world_map/layers/quest_poi_layer.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// quest_poi_layer.cpp — Quest objective markers on the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/quest_poi_layer.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include "core/coordinates.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void QuestPOILayer::render(const LayerContext& ctx) {
|
||||||
|
if (!pois_ || pois_->empty()) return;
|
||||||
|
if (ctx.currentZoneIdx < 0) return;
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
ZoneBounds bounds = zone.bounds;
|
||||||
|
bool isContinent = zone.areaID == 0;
|
||||||
|
if (isContinent) {
|
||||||
|
float l, r, t, b;
|
||||||
|
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||||
|
bounds = {l, r, t, b};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 mp = ImGui::GetMousePos();
|
||||||
|
ImFont* qFont = ImGui::GetFont();
|
||||||
|
for (const auto& qp : *pois_) {
|
||||||
|
glm::vec3 rPos = core::coords::canonicalToRender(
|
||||||
|
glm::vec3(qp.wowX, qp.wowY, 0.0f));
|
||||||
|
glm::vec2 uv = renderPosToMapUV(rPos, bounds, isContinent);
|
||||||
|
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||||
|
|
||||||
|
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||||
|
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||||
|
|
||||||
|
ctx.drawList->AddCircleFilled(ImVec2(px, py), 5.0f, IM_COL32(0, 210, 255, 220));
|
||||||
|
ctx.drawList->AddCircle(ImVec2(px, py), 5.0f, IM_COL32(255, 215, 0, 220), 0, 1.5f);
|
||||||
|
|
||||||
|
if (!qp.name.empty()) {
|
||||||
|
ImVec2 nameSz = qFont->CalcTextSizeA(ImGui::GetFontSize() * 0.85f, FLT_MAX, 0.0f, qp.name.c_str());
|
||||||
|
float tx = px - nameSz.x * 0.5f;
|
||||||
|
float ty = py - nameSz.y - 7.0f;
|
||||||
|
ctx.drawList->AddText(qFont, ImGui::GetFontSize() * 0.85f,
|
||||||
|
ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), qp.name.c_str());
|
||||||
|
ctx.drawList->AddText(qFont, ImGui::GetFontSize() * 0.85f,
|
||||||
|
ImVec2(tx, ty), IM_COL32(255, 230, 100, 230), qp.name.c_str());
|
||||||
|
}
|
||||||
|
float mdx = mp.x - px, mdy = mp.y - py;
|
||||||
|
if (mdx * mdx + mdy * mdy < 49.0f && !qp.name.empty()) {
|
||||||
|
ImGui::SetTooltip("%s\n(Quest Objective)", qp.name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
100
src/rendering/world_map/layers/subzone_tooltip_layer.cpp
Normal file
100
src/rendering/world_map/layers/subzone_tooltip_layer.cpp
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
// subzone_tooltip_layer.cpp — Overlay area hover labels in zone view.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/subzone_tooltip_layer.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void SubzoneTooltipLayer::render(const LayerContext& ctx) {
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE) return;
|
||||||
|
if (ctx.currentZoneIdx < 0 || !ctx.zones) return;
|
||||||
|
|
||||||
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||||
|
if (mp.x < ctx.imgMin.x || mp.x > ctx.imgMin.x + ctx.displayW ||
|
||||||
|
mp.y < ctx.imgMin.y || mp.y > ctx.imgMin.y + ctx.displayH)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float mu = (mp.x - ctx.imgMin.x) / ctx.displayW;
|
||||||
|
float mv = (mp.y - ctx.imgMin.y) / ctx.displayH;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
std::string hoveredName;
|
||||||
|
bool hoveredExplored = false;
|
||||||
|
float bestArea = std::numeric_limits<float>::max();
|
||||||
|
|
||||||
|
float fboW = static_cast<float>(ctx.fboW);
|
||||||
|
float fboH = static_cast<float>(ctx.fboH);
|
||||||
|
|
||||||
|
// Mouse position in FBO pixel coordinates (used for HitRect AABB test)
|
||||||
|
float pixelX = mu * fboW;
|
||||||
|
float pixelY = mv * fboH;
|
||||||
|
|
||||||
|
for (int oi = 0; oi < static_cast<int>(zone.overlays.size()); oi++) {
|
||||||
|
const auto& ov = zone.overlays[oi];
|
||||||
|
|
||||||
|
// ── Hybrid Approach: Zone view uses HitRect AABB pre-filter ──
|
||||||
|
// WorldMapOverlay.dbc fields 13-16 define a hit-test rectangle.
|
||||||
|
// Only overlays whose HitRect contains the mouse need further testing.
|
||||||
|
// This is Blizzard's optimization to avoid sampling every overlay.
|
||||||
|
bool hasHitRect = (ov.hitRectRight > ov.hitRectLeft &&
|
||||||
|
ov.hitRectBottom > ov.hitRectTop);
|
||||||
|
if (hasHitRect) {
|
||||||
|
if (pixelX < static_cast<float>(ov.hitRectLeft) ||
|
||||||
|
pixelX > static_cast<float>(ov.hitRectRight) ||
|
||||||
|
pixelY < static_cast<float>(ov.hitRectTop) ||
|
||||||
|
pixelY > static_cast<float>(ov.hitRectBottom)) {
|
||||||
|
continue; // Mouse outside HitRect — skip this overlay
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: use overlay offset+size AABB (old behaviour)
|
||||||
|
float ovLeft = static_cast<float>(ov.offsetX) / fboW;
|
||||||
|
float ovTop = static_cast<float>(ov.offsetY) / fboH;
|
||||||
|
float ovRight = static_cast<float>(ov.offsetX + ov.texWidth) / fboW;
|
||||||
|
float ovBottom = static_cast<float>(ov.offsetY + ov.texHeight) / fboH;
|
||||||
|
|
||||||
|
if (mu < ovLeft || mu > ovRight || mv < ovTop || mv > ovBottom)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float area = static_cast<float>(ov.texWidth) * static_cast<float>(ov.texHeight);
|
||||||
|
if (area < bestArea) {
|
||||||
|
bestArea = area;
|
||||||
|
// Find display name from the first valid area ID
|
||||||
|
for (int a = 0; a < 4; a++) {
|
||||||
|
if (ov.areaIDs[a] == 0) continue;
|
||||||
|
if (ctx.areaNameByAreaId) {
|
||||||
|
auto nameIt = ctx.areaNameByAreaId->find(ov.areaIDs[a]);
|
||||||
|
if (nameIt != ctx.areaNameByAreaId->end()) {
|
||||||
|
hoveredName = nameIt->second;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hoveredExplored = ctx.exploredOverlays &&
|
||||||
|
ctx.exploredOverlays->count(oi) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hoveredName.empty()) {
|
||||||
|
std::string label = hoveredName;
|
||||||
|
if (!hoveredExplored)
|
||||||
|
label += " (Unexplored)";
|
||||||
|
|
||||||
|
ImVec2 labelSz = ImGui::CalcTextSize(label.c_str());
|
||||||
|
float lx = ctx.imgMin.x + (ctx.displayW - labelSz.x) * 0.5f;
|
||||||
|
float ly = ctx.imgMin.y + 6.0f;
|
||||||
|
ImU32 labelCol = hoveredExplored
|
||||||
|
? IM_COL32(255, 230, 150, 240)
|
||||||
|
: IM_COL32(160, 160, 160, 200);
|
||||||
|
ctx.drawList->AddText(ImVec2(lx + 1.0f, ly + 1.0f),
|
||||||
|
IM_COL32(0, 0, 0, 200), label.c_str());
|
||||||
|
ctx.drawList->AddText(ImVec2(lx, ly), labelCol, label.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
62
src/rendering/world_map/layers/taxi_node_layer.cpp
Normal file
62
src/rendering/world_map/layers/taxi_node_layer.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
// taxi_node_layer.cpp — Flight master diamond icons on the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/taxi_node_layer.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include "core/coordinates.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void TaxiNodeLayer::render(const LayerContext& ctx) {
|
||||||
|
if (!nodes_ || nodes_->empty()) return;
|
||||||
|
if (ctx.currentZoneIdx < 0) return;
|
||||||
|
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||||
|
ZoneBounds bounds = zone.bounds;
|
||||||
|
bool isContinent = zone.areaID == 0;
|
||||||
|
if (isContinent) {
|
||||||
|
float l, r, t, b;
|
||||||
|
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||||
|
bounds = {l, r, t, b};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 mp = ImGui::GetMousePos();
|
||||||
|
for (const auto& node : *nodes_) {
|
||||||
|
if (!node.known) continue;
|
||||||
|
if (static_cast<int>(node.mapId) != ctx.currentMapId) continue;
|
||||||
|
|
||||||
|
glm::vec3 rPos = core::coords::canonicalToRender(
|
||||||
|
glm::vec3(node.wowX, node.wowY, node.wowZ));
|
||||||
|
glm::vec2 uv = renderPosToMapUV(rPos, bounds, isContinent);
|
||||||
|
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||||
|
|
||||||
|
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||||
|
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||||
|
|
||||||
|
constexpr float H = 5.0f;
|
||||||
|
ImVec2 top2(px, py - H);
|
||||||
|
ImVec2 right2(px + H, py );
|
||||||
|
ImVec2 bot2(px, py + H);
|
||||||
|
ImVec2 left2(px - H, py );
|
||||||
|
ctx.drawList->AddQuadFilled(top2, right2, bot2, left2,
|
||||||
|
IM_COL32(255, 215, 0, 230));
|
||||||
|
ctx.drawList->AddQuad(top2, right2, bot2, left2,
|
||||||
|
IM_COL32(80, 50, 0, 200), 1.2f);
|
||||||
|
|
||||||
|
if (!node.name.empty()) {
|
||||||
|
float mdx = mp.x - px, mdy = mp.y - py;
|
||||||
|
if (mdx * mdx + mdy * mdy < 49.0f) {
|
||||||
|
ImGui::SetTooltip("%s\n(Flight Master)", node.name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
294
src/rendering/world_map/layers/zone_highlight_layer.cpp
Normal file
294
src/rendering/world_map/layers/zone_highlight_layer.cpp
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
// zone_highlight_layer.cpp — Continent view zone rectangles + hover effects.
|
||||||
|
// Extracted from WorldMap::renderZoneHighlights (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/layers/zone_highlight_layer.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include "rendering/vk_texture.hpp"
|
||||||
|
#include "rendering/vk_context.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <backends/imgui_impl_vulkan.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
ZoneHighlightLayer::~ZoneHighlightLayer() {
|
||||||
|
// At shutdown vkDeviceWaitIdle has been called, so immediate cleanup is safe.
|
||||||
|
if (vkCtx_) {
|
||||||
|
VkDevice device = vkCtx_->getDevice();
|
||||||
|
VmaAllocator alloc = vkCtx_->getAllocator();
|
||||||
|
for (auto& [name, entry] : highlights_) {
|
||||||
|
if (entry.imguiDS) ImGui_ImplVulkan_RemoveTexture(entry.imguiDS);
|
||||||
|
if (entry.texture) entry.texture->destroy(device, alloc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
highlights_.clear();
|
||||||
|
missingHighlights_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoneHighlightLayer::initialize(VkContext* ctx, pipeline::AssetManager* am) {
|
||||||
|
vkCtx_ = ctx;
|
||||||
|
assetManager_ = am;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoneHighlightLayer::clearTextures() {
|
||||||
|
if (vkCtx_ && !highlights_.empty()) {
|
||||||
|
// Defer destruction until all in-flight frames complete.
|
||||||
|
// The previous frame's command buffer may still reference these ImGui
|
||||||
|
// descriptor sets and texture image views from highlight draw commands.
|
||||||
|
VkDevice device = vkCtx_->getDevice();
|
||||||
|
VmaAllocator alloc = vkCtx_->getAllocator();
|
||||||
|
|
||||||
|
struct DeferredHighlight {
|
||||||
|
std::unique_ptr<VkTexture> texture;
|
||||||
|
VkDescriptorSet imguiDS;
|
||||||
|
};
|
||||||
|
auto captured = std::make_shared<std::vector<DeferredHighlight>>();
|
||||||
|
for (auto& [name, entry] : highlights_) {
|
||||||
|
DeferredHighlight dh;
|
||||||
|
dh.texture = std::move(entry.texture);
|
||||||
|
dh.imguiDS = entry.imguiDS;
|
||||||
|
captured->push_back(std::move(dh));
|
||||||
|
}
|
||||||
|
vkCtx_->deferAfterAllFrameFences([device, alloc, captured]() {
|
||||||
|
for (auto& dh : *captured) {
|
||||||
|
if (dh.imguiDS) ImGui_ImplVulkan_RemoveTexture(dh.imguiDS);
|
||||||
|
if (dh.texture) dh.texture->destroy(device, alloc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
highlights_.clear();
|
||||||
|
missingHighlights_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoneHighlightLayer::ensureHighlight(const std::string& key,
|
||||||
|
const std::string& customPath) {
|
||||||
|
if (!vkCtx_ || !assetManager_) return;
|
||||||
|
if (key.empty()) return;
|
||||||
|
if (highlights_.count(key) || missingHighlights_.count(key)) return;
|
||||||
|
|
||||||
|
// Determine BLP path
|
||||||
|
std::string path;
|
||||||
|
if (!customPath.empty()) {
|
||||||
|
path = customPath;
|
||||||
|
} else {
|
||||||
|
std::string lower = key;
|
||||||
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
path = "Interface\\WorldMap\\" + key + "\\" + lower + "highlight.blp";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto blpImage = assetManager_->loadTexture(path);
|
||||||
|
if (!blpImage.isValid()) {
|
||||||
|
LOG_WARNING("ZoneHighlightLayer: highlight not found for key='", key, "' path='", path, "'");
|
||||||
|
missingHighlights_.insert(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("ZoneHighlightLayer: loaded highlight key='", key, "' path='", path,
|
||||||
|
"' ", blpImage.width, "x", blpImage.height, " dataSize=", blpImage.data.size());
|
||||||
|
|
||||||
|
// WoW highlight BLPs with alphaDepth=0 use additive blending (white=glow, black=invisible).
|
||||||
|
// Convert to alpha-blend compatible: set alpha = max(R,G,B) for fully opaque textures.
|
||||||
|
{
|
||||||
|
bool allOpaque = true;
|
||||||
|
for (size_t i = 3; i < blpImage.data.size(); i += 4) {
|
||||||
|
if (blpImage.data[i] < 255) { allOpaque = false; break; }
|
||||||
|
}
|
||||||
|
if (allOpaque) {
|
||||||
|
for (size_t i = 0; i < blpImage.data.size(); i += 4) {
|
||||||
|
uint8_t r = blpImage.data[i], g = blpImage.data[i + 1], b = blpImage.data[i + 2];
|
||||||
|
blpImage.data[i + 3] = std::max({r, g, b});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkDevice device = vkCtx_->getDevice();
|
||||||
|
|
||||||
|
auto tex = std::make_unique<VkTexture>();
|
||||||
|
if (!tex->upload(*vkCtx_, blpImage.data.data(), blpImage.width, blpImage.height,
|
||||||
|
VK_FORMAT_R8G8B8A8_UNORM, false)) {
|
||||||
|
missingHighlights_.insert(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f)) {
|
||||||
|
tex->destroy(device, vkCtx_->getAllocator());
|
||||||
|
missingHighlights_.insert(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkDescriptorSet ds = ImGui_ImplVulkan_AddTexture(
|
||||||
|
tex->getSampler(), tex->getImageView(),
|
||||||
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
|
|
||||||
|
if (!ds) {
|
||||||
|
tex->destroy(device, vkCtx_->getAllocator());
|
||||||
|
missingHighlights_.insert(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightEntry entry;
|
||||||
|
entry.texture = std::move(tex);
|
||||||
|
entry.imguiDS = ds;
|
||||||
|
highlights_[key] = std::move(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImTextureID ZoneHighlightLayer::getHighlightTexture(const std::string& key,
|
||||||
|
const std::string& customPath) {
|
||||||
|
ensureHighlight(key, customPath);
|
||||||
|
auto it = highlights_.find(key);
|
||||||
|
if (it != highlights_.end() && it->second.imguiDS) {
|
||||||
|
return reinterpret_cast<ImTextureID>(it->second.imguiDS);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoneHighlightLayer::render(const LayerContext& ctx) {
|
||||||
|
if (ctx.viewLevel != ViewLevel::CONTINENT || ctx.continentIdx < 0) return;
|
||||||
|
if (!ctx.zones) return;
|
||||||
|
|
||||||
|
const auto& cont = (*ctx.zones)[ctx.continentIdx];
|
||||||
|
float cLeft = cont.bounds.locLeft, cRight = cont.bounds.locRight;
|
||||||
|
float cTop = cont.bounds.locTop, cBottom = cont.bounds.locBottom;
|
||||||
|
getContinentProjectionBounds(*ctx.zones, ctx.continentIdx, cLeft, cRight, cTop, cBottom);
|
||||||
|
float cDenomU = cLeft - cRight;
|
||||||
|
float cDenomV = cTop - cBottom;
|
||||||
|
|
||||||
|
if (std::abs(cDenomU) < 0.001f || std::abs(cDenomV) < 0.001f) return;
|
||||||
|
|
||||||
|
hoveredZone_ = -1;
|
||||||
|
ImVec2 mousePos = ImGui::GetMousePos();
|
||||||
|
|
||||||
|
// ── Render zone rectangles using DBC world-coord AABB projection ──
|
||||||
|
// (Restored from old WorldMap::renderImGuiOverlay — no ZMP dependency)
|
||||||
|
for (int zi = 0; zi < static_cast<int>(ctx.zones->size()); zi++) {
|
||||||
|
if (!zoneBelongsToContinent(*ctx.zones, zi, ctx.continentIdx)) continue;
|
||||||
|
const auto& z = (*ctx.zones)[zi];
|
||||||
|
if (std::abs(z.bounds.locLeft - z.bounds.locRight) < 0.001f ||
|
||||||
|
std::abs(z.bounds.locTop - z.bounds.locBottom) < 0.001f) continue;
|
||||||
|
|
||||||
|
// Project from WorldMapArea.dbc world coords
|
||||||
|
float zuMin = (cLeft - z.bounds.locLeft) / cDenomU;
|
||||||
|
float zuMax = (cLeft - z.bounds.locRight) / cDenomU;
|
||||||
|
float zvMin = (cTop - z.bounds.locTop) / cDenomV;
|
||||||
|
float zvMax = (cTop - z.bounds.locBottom) / cDenomV;
|
||||||
|
|
||||||
|
constexpr float kOverlayShrink = 0.92f;
|
||||||
|
float cu = (zuMin + zuMax) * 0.5f, cv = (zvMin + zvMax) * 0.5f;
|
||||||
|
float hu = (zuMax - zuMin) * 0.5f * kOverlayShrink;
|
||||||
|
float hv = (zvMax - zvMin) * 0.5f * kOverlayShrink;
|
||||||
|
zuMin = cu - hu; zuMax = cu + hu;
|
||||||
|
zvMin = cv - hv; zvMax = cv + hv;
|
||||||
|
|
||||||
|
constexpr float kVOffset = -0.15f;
|
||||||
|
zvMin = (zvMin - 0.5f) + 0.5f + kVOffset;
|
||||||
|
zvMax = (zvMax - 0.5f) + 0.5f + kVOffset;
|
||||||
|
|
||||||
|
zuMin = std::clamp(zuMin, 0.0f, 1.0f);
|
||||||
|
zuMax = std::clamp(zuMax, 0.0f, 1.0f);
|
||||||
|
zvMin = std::clamp(zvMin, 0.0f, 1.0f);
|
||||||
|
zvMax = std::clamp(zvMax, 0.0f, 1.0f);
|
||||||
|
if (zuMax - zuMin < 0.001f || zvMax - zvMin < 0.001f) continue;
|
||||||
|
|
||||||
|
float sx0 = ctx.imgMin.x + zuMin * ctx.displayW;
|
||||||
|
float sy0 = ctx.imgMin.y + zvMin * ctx.displayH;
|
||||||
|
float sx1 = ctx.imgMin.x + zuMax * ctx.displayW;
|
||||||
|
float sy1 = ctx.imgMin.y + zvMax * ctx.displayH;
|
||||||
|
|
||||||
|
bool explored = !ctx.exploredZones ||
|
||||||
|
ctx.exploredZones->empty() ||
|
||||||
|
ctx.exploredZones->count(zi) > 0;
|
||||||
|
bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 &&
|
||||||
|
mousePos.y >= sy0 && mousePos.y <= sy1);
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
hoveredZone_ = zi;
|
||||||
|
|
||||||
|
if (prevHoveredZone_ == zi) {
|
||||||
|
hoverHighlightAlpha_ = std::min(hoverHighlightAlpha_ + 0.08f, 1.0f);
|
||||||
|
} else {
|
||||||
|
hoverHighlightAlpha_ = 0.3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the highlight BLP texture within the zone's bounding rectangle.
|
||||||
|
auto it = highlights_.find(z.areaName);
|
||||||
|
if (it == highlights_.end()) ensureHighlight(z.areaName, "");
|
||||||
|
it = highlights_.find(z.areaName);
|
||||||
|
if (it != highlights_.end() && it->second.imguiDS) {
|
||||||
|
uint8_t imgAlpha = static_cast<uint8_t>(255.0f * hoverHighlightAlpha_);
|
||||||
|
// Draw twice for a very bright glow effect
|
||||||
|
ctx.drawList->AddImage(
|
||||||
|
reinterpret_cast<ImTextureID>(it->second.imguiDS),
|
||||||
|
ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
|
ImVec2(0, 0), ImVec2(1, 1),
|
||||||
|
IM_COL32(255, 255, 255, imgAlpha));
|
||||||
|
ctx.drawList->AddImage(
|
||||||
|
reinterpret_cast<ImTextureID>(it->second.imguiDS),
|
||||||
|
ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
|
ImVec2(0, 0), ImVec2(1, 1),
|
||||||
|
IM_COL32(255, 255, 200, imgAlpha));
|
||||||
|
} else {
|
||||||
|
// Fallback: bright colored rectangle if no highlight texture
|
||||||
|
uint8_t fillAlpha = static_cast<uint8_t>(100.0f * hoverHighlightAlpha_);
|
||||||
|
ctx.drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
|
IM_COL32(255, 235, 50, fillAlpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t borderAlpha = static_cast<uint8_t>(200.0f * hoverHighlightAlpha_);
|
||||||
|
ctx.drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
|
IM_COL32(255, 225, 50, borderAlpha), 0, 0, 2.0f);
|
||||||
|
} else if (explored) {
|
||||||
|
ctx.drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
|
IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone name label
|
||||||
|
bool zoneExplored = explored;
|
||||||
|
if (!z.areaName.empty()) {
|
||||||
|
const ZoneMeta* meta = metadata_ ? metadata_->find(z.areaName) : nullptr;
|
||||||
|
std::string label = ZoneMetadata::formatLabel(z.areaName, meta);
|
||||||
|
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
float fontSize = ImGui::GetFontSize() * 0.75f;
|
||||||
|
ImVec2 labelSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, label.c_str());
|
||||||
|
float zoneCx = (sx0 + sx1) * 0.5f;
|
||||||
|
float zoneCy = (sy0 + sy1) * 0.5f;
|
||||||
|
float lx = zoneCx - labelSz.x * 0.5f;
|
||||||
|
float ly = zoneCy - labelSz.y * 0.5f;
|
||||||
|
|
||||||
|
if (labelSz.x < (sx1 - sx0) * 1.1f && labelSz.y < (sy1 - sy0) * 0.8f) {
|
||||||
|
ImU32 textColor;
|
||||||
|
if (!zoneExplored) {
|
||||||
|
textColor = IM_COL32(140, 140, 140, 130);
|
||||||
|
} else if (meta) {
|
||||||
|
switch (meta->faction) {
|
||||||
|
case ZoneFaction::Alliance: textColor = IM_COL32(100, 160, 255, 200); break;
|
||||||
|
case ZoneFaction::Horde: textColor = IM_COL32(255, 100, 100, 200); break;
|
||||||
|
case ZoneFaction::Contested: textColor = IM_COL32(255, 215, 0, 190); break;
|
||||||
|
default: textColor = IM_COL32(255, 230, 180, 180); break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textColor = IM_COL32(255, 230, 180, 180);
|
||||||
|
}
|
||||||
|
ctx.drawList->AddText(font, fontSize,
|
||||||
|
ImVec2(lx + 1.0f, ly + 1.0f),
|
||||||
|
IM_COL32(0, 0, 0, 140), label.c_str());
|
||||||
|
ctx.drawList->AddText(font, fontSize,
|
||||||
|
ImVec2(lx, ly), textColor, label.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevHoveredZone_ = hoveredZone_;
|
||||||
|
if (hoveredZone_ < 0) {
|
||||||
|
hoverHighlightAlpha_ = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
206
src/rendering/world_map/map_resolver.cpp
Normal file
206
src/rendering/world_map/map_resolver.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
// map_resolver.cpp — Centralized map navigation resolution for the world map.
|
||||||
|
// Map folder names resolved from a built-in table matching
|
||||||
|
// Data/interface/worldmap/ — no dependency on WorldLoader.
|
||||||
|
#include "rendering/world_map/map_resolver.hpp"
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
// ── Worldmap folder table (from Data/interface/worldmap/) ────
|
||||||
|
// Each entry maps a DBC MapID to its worldmap folder name and UI display name.
|
||||||
|
// Folder names match the directories under Data/interface/worldmap/.
|
||||||
|
|
||||||
|
struct MapFolderEntry {
|
||||||
|
uint32_t mapId;
|
||||||
|
const char* folder; // worldmap folder name (case as on disk)
|
||||||
|
const char* displayName; // UI display name
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr MapFolderEntry kMapFolders[] = {
|
||||||
|
// Special UI-only views (no DBC MapID — sentinel values)
|
||||||
|
{ UINT32_MAX, "World", "World" },
|
||||||
|
{ UINT32_MAX - 1, "Cosmic", "Cosmic" },
|
||||||
|
// Continents
|
||||||
|
{ 0, "Azeroth", "Eastern Kingdoms" },
|
||||||
|
{ 1, "Kalimdor", "Kalimdor" },
|
||||||
|
{ 530, "Expansion01", "Outland" },
|
||||||
|
{ 571, "Northrend", "Northrend" },
|
||||||
|
// Dungeons / instances with worldmap folders
|
||||||
|
// (Data/interface/worldmap/<folder>/ exists for these)
|
||||||
|
{ 33, "Shadowfang", "Shadowfang Keep" }, // placeholder – no folder yet
|
||||||
|
{ 209, "Tanaris", "Tanaris" }, // shared with zone
|
||||||
|
{ 534, "CoTStratholme", "Caverns of Time" },
|
||||||
|
{ 574, "UtgardeKeep", "Utgarde Keep" },
|
||||||
|
{ 575, "UtgardePinnacle", "Utgarde Pinnacle" },
|
||||||
|
{ 578, "Nexus80", "The Nexus" },
|
||||||
|
{ 595, "ThecullingOfStratholme","Culling of Stratholme" },
|
||||||
|
{ 599, "HallsOfLightning", "Halls of Lightning" },
|
||||||
|
{ 600, "HallsOfStone", "Halls of Stone" },
|
||||||
|
{ 601, "DrakTheron", "Drak'Theron Keep" },
|
||||||
|
{ 602, "GunDrak", "Gundrak" },
|
||||||
|
{ 603, "Ulduar77", "Ulduar" },
|
||||||
|
{ 608, "VioletHold", "Violet Hold" },
|
||||||
|
{ 619, "AhnKahet", "Ahn'kahet" },
|
||||||
|
{ 631, "IcecrownCitadel", "Icecrown Citadel" },
|
||||||
|
{ 632, "TheForgeOfSouls", "Forge of Souls" },
|
||||||
|
{ 649, "TheArgentColiseum", "Trial of the Crusader" },
|
||||||
|
{ 658, "PitOfSaron", "Pit of Saron" },
|
||||||
|
{ 668, "HallsOfReflection", "Halls of Reflection" },
|
||||||
|
{ 724, "TheRubySanctum", "Ruby Sanctum" },
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int kMapFolderCount = sizeof(kMapFolders) / sizeof(kMapFolders[0]);
|
||||||
|
|
||||||
|
// ── Map folder lookup functions ──────────────────────────────
|
||||||
|
|
||||||
|
const char* mapIdToFolder(uint32_t mapId) {
|
||||||
|
for (int i = 0; i < kMapFolderCount; i++) {
|
||||||
|
if (kMapFolders[i].mapId == mapId)
|
||||||
|
return kMapFolders[i].folder;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
int folderToMapId(const std::string& folder) {
|
||||||
|
for (int i = 0; i < kMapFolderCount; i++) {
|
||||||
|
// Case-insensitive compare
|
||||||
|
const char* entry = kMapFolders[i].folder;
|
||||||
|
if (folder.size() != std::char_traits<char>::length(entry)) continue;
|
||||||
|
bool match = true;
|
||||||
|
for (size_t j = 0; j < folder.size(); j++) {
|
||||||
|
if (std::tolower(static_cast<unsigned char>(folder[j])) !=
|
||||||
|
std::tolower(static_cast<unsigned char>(entry[j]))) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) return static_cast<int>(kMapFolders[i].mapId);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* mapDisplayName(uint32_t mapId) {
|
||||||
|
for (int i = 0; i < kMapFolderCount; i++) {
|
||||||
|
if (kMapFolders[i].mapId == mapId)
|
||||||
|
return kMapFolders[i].displayName;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helper: find best continent zone for a mapId ─────────────
|
||||||
|
|
||||||
|
int findContinentForMapId(const std::vector<Zone>& zones,
|
||||||
|
uint32_t mapId,
|
||||||
|
int cosmicIdx) {
|
||||||
|
// 1) Prefer a leaf continent whose displayMapID matches the target mapId.
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
if (i == cosmicIdx) continue;
|
||||||
|
if (zones[i].areaID != 0) continue;
|
||||||
|
if (isLeafContinent(zones, i) && zones[i].displayMapID == mapId)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Find the first non-root, non-cosmic continent.
|
||||||
|
int firstContinent = -1;
|
||||||
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||||
|
if (i == cosmicIdx) continue;
|
||||||
|
if (zones[i].areaID != 0) continue;
|
||||||
|
if (firstContinent < 0) firstContinent = i;
|
||||||
|
if (!isRootContinent(zones, i)) return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Fallback to first continent entry
|
||||||
|
return firstContinent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resolve WORLD view region click ──────────────────────────
|
||||||
|
|
||||||
|
MapResolveResult resolveWorldRegionClick(uint32_t regionMapId,
|
||||||
|
const std::vector<Zone>& zones,
|
||||||
|
int currentMapId,
|
||||||
|
int cosmicIdx) {
|
||||||
|
MapResolveResult result;
|
||||||
|
|
||||||
|
if (static_cast<int>(regionMapId) == currentMapId) {
|
||||||
|
// Target map is already loaded — navigate to the matching continent
|
||||||
|
// within the current zone data (no reload needed).
|
||||||
|
int contIdx = findContinentForMapId(zones, regionMapId, cosmicIdx);
|
||||||
|
if (contIdx >= 0) {
|
||||||
|
result.action = MapResolveAction::NAVIGATE_CONTINENT;
|
||||||
|
result.targetZoneIdx = contIdx;
|
||||||
|
LOG_INFO("resolveWorldRegionClick: mapId=", regionMapId,
|
||||||
|
" matches current map — NAVIGATE_CONTINENT idx=", contIdx);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("resolveWorldRegionClick: mapId=", regionMapId,
|
||||||
|
" matches current but no continent found");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different map — need to load it
|
||||||
|
const char* folder = mapIdToFolder(regionMapId);
|
||||||
|
if (folder[0]) {
|
||||||
|
result.action = MapResolveAction::LOAD_MAP;
|
||||||
|
result.targetMapName = folder;
|
||||||
|
LOG_INFO("resolveWorldRegionClick: mapId=", regionMapId,
|
||||||
|
" → LOAD_MAP '", folder, "'");
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("resolveWorldRegionClick: unknown mapId=", regionMapId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resolve CONTINENT view zone click ────────────────────────
|
||||||
|
|
||||||
|
MapResolveResult resolveZoneClick(int zoneIdx,
|
||||||
|
const std::vector<Zone>& zones,
|
||||||
|
int currentMapId) {
|
||||||
|
MapResolveResult result;
|
||||||
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return result;
|
||||||
|
|
||||||
|
const auto& zone = zones[zoneIdx];
|
||||||
|
|
||||||
|
// If the zone's displayMapID differs from the current map, it belongs to
|
||||||
|
// a different continent/map. Load that map instead.
|
||||||
|
// Skip sentinel values (UINT32_MAX / UINT32_MAX-1) used by kMapFolders for
|
||||||
|
// World/Cosmic; the DBC stores -1 (0xFFFFFFFF) to mean "no display map".
|
||||||
|
if (zone.displayMapID != 0 &&
|
||||||
|
zone.displayMapID < UINT32_MAX - 1 &&
|
||||||
|
static_cast<int>(zone.displayMapID) != currentMapId) {
|
||||||
|
const char* folder = mapIdToFolder(zone.displayMapID);
|
||||||
|
if (folder[0]) {
|
||||||
|
result.action = MapResolveAction::LOAD_MAP;
|
||||||
|
result.targetMapName = folder;
|
||||||
|
LOG_INFO("resolveZoneClick: zone[", zoneIdx, "] '", zone.areaName,
|
||||||
|
"' displayMapID=", zone.displayMapID, " → LOAD_MAP '", folder, "'");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal case: enter the zone within the current map
|
||||||
|
result.action = MapResolveAction::ENTER_ZONE;
|
||||||
|
result.targetZoneIdx = zoneIdx;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resolve COSMIC view click ────────────────────────────────
|
||||||
|
|
||||||
|
MapResolveResult resolveCosmicClick(uint32_t targetMapId) {
|
||||||
|
MapResolveResult result;
|
||||||
|
const char* folder = mapIdToFolder(targetMapId);
|
||||||
|
if (folder[0]) {
|
||||||
|
result.action = MapResolveAction::LOAD_MAP;
|
||||||
|
result.targetMapName = folder;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
21
src/rendering/world_map/overlay_renderer.cpp
Normal file
21
src/rendering/world_map/overlay_renderer.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// overlay_renderer.cpp — ImGui overlay orchestrator for the world map.
|
||||||
|
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||||
|
#include "rendering/world_map/overlay_renderer.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void OverlayRenderer::addLayer(std::unique_ptr<IOverlayLayer> layer) {
|
||||||
|
layers_.push_back(std::move(layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OverlayRenderer::render(const LayerContext& ctx) {
|
||||||
|
for (auto& layer : layers_) {
|
||||||
|
layer->render(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
141
src/rendering/world_map/view_state_machine.cpp
Normal file
141
src/rendering/world_map/view_state_machine.cpp
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// view_state_machine.cpp — Navigation state and transitions for the world map.
|
||||||
|
// Extracted from WorldMap::zoomIn, zoomOut, enterWorldView, enterCosmicView
|
||||||
|
// (Phase 6 of refactoring plan).
|
||||||
|
#include "rendering/world_map/view_state_machine.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void ViewStateMachine::startTransition(ViewLevel from, ViewLevel to, float duration) {
|
||||||
|
transition_.active = true;
|
||||||
|
transition_.progress = 0.0f;
|
||||||
|
transition_.duration = duration;
|
||||||
|
transition_.fromLevel = from;
|
||||||
|
transition_.toLevel = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ViewStateMachine::updateTransition(float deltaTime) {
|
||||||
|
if (!transition_.active) return false;
|
||||||
|
transition_.progress += deltaTime / transition_.duration;
|
||||||
|
if (transition_.progress >= 1.0f) {
|
||||||
|
transition_.progress = 1.0f;
|
||||||
|
transition_.active = false;
|
||||||
|
}
|
||||||
|
return transition_.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewStateMachine::ZoomResult ViewStateMachine::zoomIn(int hoveredZoneIdx, int playerZoneIdx) {
|
||||||
|
ZoomResult result;
|
||||||
|
|
||||||
|
if (level_ == ViewLevel::COSMIC) {
|
||||||
|
startTransition(ViewLevel::COSMIC, ViewLevel::WORLD);
|
||||||
|
level_ = ViewLevel::WORLD;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::WORLD;
|
||||||
|
// Caller should call enterWorldView() to determine target index
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level_ == ViewLevel::WORLD) {
|
||||||
|
if (continentIdx_ >= 0) {
|
||||||
|
startTransition(ViewLevel::WORLD, ViewLevel::CONTINENT);
|
||||||
|
level_ = ViewLevel::CONTINENT;
|
||||||
|
currentIdx_ = continentIdx_;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::CONTINENT;
|
||||||
|
result.targetIdx = continentIdx_;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level_ == ViewLevel::CONTINENT) {
|
||||||
|
// Prefer the zone the mouse is hovering over; fall back to the player's zone
|
||||||
|
int zoneIdx = hoveredZoneIdx >= 0 ? hoveredZoneIdx : playerZoneIdx;
|
||||||
|
if (zoneIdx >= 0) {
|
||||||
|
startTransition(ViewLevel::CONTINENT, ViewLevel::ZONE);
|
||||||
|
level_ = ViewLevel::ZONE;
|
||||||
|
currentIdx_ = zoneIdx;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::ZONE;
|
||||||
|
result.targetIdx = zoneIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewStateMachine::ZoomResult ViewStateMachine::zoomOut() {
|
||||||
|
ZoomResult result;
|
||||||
|
|
||||||
|
if (level_ == ViewLevel::ZONE) {
|
||||||
|
if (continentIdx_ >= 0) {
|
||||||
|
startTransition(ViewLevel::ZONE, ViewLevel::CONTINENT);
|
||||||
|
level_ = ViewLevel::CONTINENT;
|
||||||
|
currentIdx_ = continentIdx_;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::CONTINENT;
|
||||||
|
result.targetIdx = continentIdx_;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level_ == ViewLevel::CONTINENT) {
|
||||||
|
startTransition(ViewLevel::CONTINENT, ViewLevel::WORLD);
|
||||||
|
level_ = ViewLevel::WORLD;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::WORLD;
|
||||||
|
// Caller should call enterWorldView() to determine target index
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level_ == ViewLevel::WORLD) {
|
||||||
|
// Vanilla: cosmic view disabled, don't zoom out further
|
||||||
|
if (!cosmicEnabled_) return result;
|
||||||
|
startTransition(ViewLevel::WORLD, ViewLevel::COSMIC);
|
||||||
|
level_ = ViewLevel::COSMIC;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::COSMIC;
|
||||||
|
// Caller should call enterCosmicView() to determine target index
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewStateMachine::ZoomResult ViewStateMachine::enterWorldView() {
|
||||||
|
ZoomResult result;
|
||||||
|
level_ = ViewLevel::WORLD;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::WORLD;
|
||||||
|
// Caller is responsible for finding the root continent and compositing
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewStateMachine::ZoomResult ViewStateMachine::enterCosmicView() {
|
||||||
|
// Vanilla: cosmic view is disabled — stay in world view
|
||||||
|
if (!cosmicEnabled_) {
|
||||||
|
return enterWorldView();
|
||||||
|
}
|
||||||
|
|
||||||
|
ZoomResult result;
|
||||||
|
level_ = ViewLevel::COSMIC;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::COSMIC;
|
||||||
|
// Caller uses cosmicIdx from DataRepository
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewStateMachine::ZoomResult ViewStateMachine::enterZone(int zoneIdx) {
|
||||||
|
ZoomResult result;
|
||||||
|
startTransition(ViewLevel::CONTINENT, ViewLevel::ZONE);
|
||||||
|
level_ = ViewLevel::ZONE;
|
||||||
|
currentIdx_ = zoneIdx;
|
||||||
|
result.changed = true;
|
||||||
|
result.newLevel = ViewLevel::ZONE;
|
||||||
|
result.targetIdx = zoneIdx;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
1060
src/rendering/world_map/world_map_facade.cpp
Normal file
1060
src/rendering/world_map/world_map_facade.cpp
Normal file
File diff suppressed because it is too large
Load diff
118
src/rendering/world_map/zone_metadata.cpp
Normal file
118
src/rendering/world_map/zone_metadata.cpp
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
// zone_metadata.cpp — Zone level ranges, faction data, and label formatting.
|
||||||
|
// Extracted from WorldMap::initZoneMeta (Phase 4 of refactoring plan).
|
||||||
|
#include "rendering/world_map/zone_metadata.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace rendering {
|
||||||
|
namespace world_map {
|
||||||
|
|
||||||
|
void ZoneMetadata::initialize() {
|
||||||
|
if (!table_.empty()) return;
|
||||||
|
|
||||||
|
// Populate known zone level ranges and faction alignment.
|
||||||
|
// This covers major open-world zones for Vanilla/TBC/WotLK.
|
||||||
|
auto add = [this](const char* name, uint8_t lo, uint8_t hi, ZoneFaction f) {
|
||||||
|
table_[name] = {lo, hi, f};
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Eastern Kingdoms ===
|
||||||
|
add("Elwynn", 1, 10, ZoneFaction::Alliance);
|
||||||
|
add("DunMorogh", 1, 10, ZoneFaction::Alliance);
|
||||||
|
add("TirisfalGlades", 1, 10, ZoneFaction::Horde);
|
||||||
|
add("Westfall", 10, 20, ZoneFaction::Alliance);
|
||||||
|
add("LochModan", 10, 20, ZoneFaction::Alliance);
|
||||||
|
add("Silverpine", 10, 20, ZoneFaction::Horde);
|
||||||
|
add("Redridge", 15, 25, ZoneFaction::Contested);
|
||||||
|
add("Duskwood", 18, 30, ZoneFaction::Alliance);
|
||||||
|
add("Wetlands", 20, 30, ZoneFaction::Alliance);
|
||||||
|
add("Hillsbrad", 20, 30, ZoneFaction::Contested);
|
||||||
|
add("Alterac", 30, 40, ZoneFaction::Contested);
|
||||||
|
add("Arathi", 30, 40, ZoneFaction::Contested);
|
||||||
|
add("StranglethornVale",30, 45, ZoneFaction::Contested);
|
||||||
|
add("Stranglethorn", 30, 45, ZoneFaction::Contested);
|
||||||
|
add("Badlands", 35, 45, ZoneFaction::Contested);
|
||||||
|
add("SwampOfSorrows", 35, 45, ZoneFaction::Contested);
|
||||||
|
add("TheBlastedLands", 45, 55, ZoneFaction::Contested);
|
||||||
|
add("SearingGorge", 43, 50, ZoneFaction::Contested);
|
||||||
|
add("BurningSteppes", 50, 58, ZoneFaction::Contested);
|
||||||
|
add("WesternPlaguelands",51,58, ZoneFaction::Contested);
|
||||||
|
add("EasternPlaguelands",53,60, ZoneFaction::Contested);
|
||||||
|
add("Hinterlands", 40, 50, ZoneFaction::Contested);
|
||||||
|
add("DeadwindPass", 55, 60, ZoneFaction::Contested);
|
||||||
|
|
||||||
|
// === Kalimdor ===
|
||||||
|
add("Durotar", 1, 10, ZoneFaction::Horde);
|
||||||
|
add("Mulgore", 1, 10, ZoneFaction::Horde);
|
||||||
|
add("Teldrassil", 1, 10, ZoneFaction::Alliance);
|
||||||
|
add("Darkshore", 10, 20, ZoneFaction::Alliance);
|
||||||
|
add("Barrens", 10, 25, ZoneFaction::Horde);
|
||||||
|
add("Ashenvale", 18, 30, ZoneFaction::Contested);
|
||||||
|
add("StonetalonMountains",15,27,ZoneFaction::Contested);
|
||||||
|
add("ThousandNeedles", 25, 35, ZoneFaction::Contested);
|
||||||
|
add("Desolace", 30, 40, ZoneFaction::Contested);
|
||||||
|
add("Dustwallow", 35, 45, ZoneFaction::Contested);
|
||||||
|
add("Feralas", 40, 50, ZoneFaction::Contested);
|
||||||
|
add("Tanaris", 40, 50, ZoneFaction::Contested);
|
||||||
|
add("Azshara", 45, 55, ZoneFaction::Contested);
|
||||||
|
add("UngoroCrater", 48, 55, ZoneFaction::Contested);
|
||||||
|
add("Felwood", 48, 55, ZoneFaction::Contested);
|
||||||
|
add("Winterspring", 55, 60, ZoneFaction::Contested);
|
||||||
|
add("Silithus", 55, 60, ZoneFaction::Contested);
|
||||||
|
add("Moonglade", 55, 60, ZoneFaction::Contested);
|
||||||
|
|
||||||
|
// === TBC: Outland ===
|
||||||
|
add("HellFire", 58, 63, ZoneFaction::Contested);
|
||||||
|
add("Zangarmarsh", 60, 64, ZoneFaction::Contested);
|
||||||
|
add("TerokkarForest", 62, 65, ZoneFaction::Contested);
|
||||||
|
add("Nagrand", 64, 67, ZoneFaction::Contested);
|
||||||
|
add("BladesEdgeMountains",65,68,ZoneFaction::Contested);
|
||||||
|
add("Netherstorm", 67, 70, ZoneFaction::Contested);
|
||||||
|
add("ShadowmoonValley",67, 70, ZoneFaction::Contested);
|
||||||
|
|
||||||
|
// === WotLK: Northrend ===
|
||||||
|
add("BoreanTundra", 68, 72, ZoneFaction::Contested);
|
||||||
|
add("HowlingFjord", 68, 72, ZoneFaction::Contested);
|
||||||
|
add("Dragonblight", 71, 75, ZoneFaction::Contested);
|
||||||
|
add("GrizzlyHills", 73, 75, ZoneFaction::Contested);
|
||||||
|
add("ZulDrak", 74, 77, ZoneFaction::Contested);
|
||||||
|
add("SholazarBasin", 76, 78, ZoneFaction::Contested);
|
||||||
|
add("StormPeaks", 77, 80, ZoneFaction::Contested);
|
||||||
|
add("Icecrown", 77, 80, ZoneFaction::Contested);
|
||||||
|
add("CrystalsongForest",77,80, ZoneFaction::Contested);
|
||||||
|
add("LakeWintergrasp", 77, 80, ZoneFaction::Contested);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZoneMeta* ZoneMetadata::find(const std::string& areaName) const {
|
||||||
|
auto it = table_.find(areaName);
|
||||||
|
return it != table_.end() ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ZoneMetadata::formatLabel(const std::string& areaName,
|
||||||
|
const ZoneMeta* meta) {
|
||||||
|
std::string label = areaName;
|
||||||
|
if (meta) {
|
||||||
|
if (meta->minLevel > 0 && meta->maxLevel > 0) {
|
||||||
|
label += " (" + std::to_string(meta->minLevel) + "-" +
|
||||||
|
std::to_string(meta->maxLevel) + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ZoneMetadata::formatHoverLabel(const std::string& areaName,
|
||||||
|
const ZoneMeta* meta) {
|
||||||
|
std::string label = formatLabel(areaName, meta);
|
||||||
|
if (meta) {
|
||||||
|
switch (meta->faction) {
|
||||||
|
case ZoneFaction::Alliance: label += " [Alliance]"; break;
|
||||||
|
case ZoneFaction::Horde: label += " [Horde]"; break;
|
||||||
|
case ZoneFaction::Contested: label += " [Contested]"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace world_map
|
||||||
|
} // namespace rendering
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "ui/quest_log_screen.hpp"
|
#include "ui/quest_log_screen.hpp"
|
||||||
#include "ui/ui_colors.hpp"
|
#include "ui/ui_colors.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
|
#include "core/world_loader.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include "rendering/renderer.hpp"
|
#include "rendering/renderer.hpp"
|
||||||
#include "rendering/vk_context.hpp"
|
#include "rendering/vk_context.hpp"
|
||||||
|
|
@ -611,13 +612,8 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
|
||||||
}
|
}
|
||||||
// Fall back to continent name if zone unavailable
|
// Fall back to continent name if zone unavailable
|
||||||
if (homeLocation.empty()) {
|
if (homeLocation.empty()) {
|
||||||
switch (mapId) {
|
const char* dn = core::WorldLoader::mapDisplayName(mapId);
|
||||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
homeLocation = dn ? dn : "Unknown";
|
||||||
case 1: homeLocation = "Kalimdor"; break;
|
|
||||||
case 530: homeLocation = "Outland"; break;
|
|
||||||
case 571: homeLocation = "Northrend"; break;
|
|
||||||
default: homeLocation = "Unknown"; break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
||||||
"Home: %s", homeLocation.c_str());
|
"Home: %s", homeLocation.c_str());
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "ui/keybinding_manager.hpp"
|
#include "ui/keybinding_manager.hpp"
|
||||||
#include "game/game_handler.hpp"
|
#include "game/game_handler.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
|
#include "core/world_loader.hpp"
|
||||||
#include "rendering/vk_context.hpp"
|
#include "rendering/vk_context.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
#include "rendering/character_preview.hpp"
|
#include "rendering/character_preview.hpp"
|
||||||
|
|
@ -2632,15 +2633,8 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
||||||
homeLocation = gameHandler_->getWhoAreaName(zoneId);
|
homeLocation = gameHandler_->getWhoAreaName(zoneId);
|
||||||
// Fall back to continent name if zone unavailable
|
// Fall back to continent name if zone unavailable
|
||||||
if (homeLocation.empty()) {
|
if (homeLocation.empty()) {
|
||||||
switch (mapId) {
|
const char* dn = core::WorldLoader::mapDisplayName(mapId);
|
||||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
homeLocation = dn ? dn : "Unknown";
|
||||||
case 1: homeLocation = "Kalimdor"; break;
|
|
||||||
case 530: homeLocation = "Outland"; break;
|
|
||||||
case 571: homeLocation = "Northrend"; break;
|
|
||||||
case 13: homeLocation = "Test"; break;
|
|
||||||
case 169: homeLocation = "Emerald Dream"; break;
|
|
||||||
default: homeLocation = "Unknown"; break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", homeLocation.c_str());
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", homeLocation.c_str());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,92 @@ endif()
|
||||||
add_test(NAME transport_components COMMAND test_transport_components)
|
add_test(NAME transport_components COMMAND test_transport_components)
|
||||||
register_test_target(test_transport_components)
|
register_test_target(test_transport_components)
|
||||||
|
|
||||||
|
# ── test_world_map ────────────────────────────────────────────
|
||||||
|
add_executable(test_world_map
|
||||||
|
test_world_map.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_world_map PRIVATE ${TEST_INCLUDE_DIRS})
|
||||||
|
target_include_directories(test_world_map SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(test_world_map PRIVATE catch2_main)
|
||||||
|
if(TARGET glm::glm)
|
||||||
|
target_link_libraries(test_world_map PRIVATE glm::glm)
|
||||||
|
endif()
|
||||||
|
add_test(NAME world_map COMMAND test_world_map)
|
||||||
|
register_test_target(test_world_map)
|
||||||
|
|
||||||
|
# ── test_world_map_coordinate_projection ──────────────────────
|
||||||
|
add_executable(test_world_map_coordinate_projection
|
||||||
|
test_world_map_coordinate_projection.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/rendering/world_map/coordinate_projection.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_world_map_coordinate_projection PRIVATE ${TEST_INCLUDE_DIRS})
|
||||||
|
target_include_directories(test_world_map_coordinate_projection SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(test_world_map_coordinate_projection PRIVATE catch2_main)
|
||||||
|
if(TARGET glm::glm)
|
||||||
|
target_link_libraries(test_world_map_coordinate_projection PRIVATE glm::glm)
|
||||||
|
endif()
|
||||||
|
add_test(NAME world_map_coordinate_projection COMMAND test_world_map_coordinate_projection)
|
||||||
|
register_test_target(test_world_map_coordinate_projection)
|
||||||
|
|
||||||
|
# ── test_world_map_map_resolver ───────────────────────────────
|
||||||
|
add_executable(test_world_map_map_resolver
|
||||||
|
test_world_map_map_resolver.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/rendering/world_map/map_resolver.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/rendering/world_map/coordinate_projection.cpp
|
||||||
|
${TEST_COMMON_SOURCES}
|
||||||
|
)
|
||||||
|
target_include_directories(test_world_map_map_resolver PRIVATE ${TEST_INCLUDE_DIRS})
|
||||||
|
target_include_directories(test_world_map_map_resolver SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(test_world_map_map_resolver PRIVATE catch2_main)
|
||||||
|
if(TARGET glm::glm)
|
||||||
|
target_link_libraries(test_world_map_map_resolver PRIVATE glm::glm)
|
||||||
|
endif()
|
||||||
|
add_test(NAME world_map_map_resolver COMMAND test_world_map_map_resolver)
|
||||||
|
register_test_target(test_world_map_map_resolver)
|
||||||
|
|
||||||
|
# ── test_world_map_view_state_machine ─────────────────────────
|
||||||
|
add_executable(test_world_map_view_state_machine
|
||||||
|
test_world_map_view_state_machine.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/rendering/world_map/view_state_machine.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_world_map_view_state_machine PRIVATE ${TEST_INCLUDE_DIRS})
|
||||||
|
target_include_directories(test_world_map_view_state_machine SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(test_world_map_view_state_machine PRIVATE catch2_main)
|
||||||
|
if(TARGET glm::glm)
|
||||||
|
target_link_libraries(test_world_map_view_state_machine PRIVATE glm::glm)
|
||||||
|
endif()
|
||||||
|
add_test(NAME world_map_view_state_machine COMMAND test_world_map_view_state_machine)
|
||||||
|
register_test_target(test_world_map_view_state_machine)
|
||||||
|
|
||||||
|
# ── test_world_map_exploration_state ──────────────────────────
|
||||||
|
add_executable(test_world_map_exploration_state
|
||||||
|
test_world_map_exploration_state.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/rendering/world_map/exploration_state.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/rendering/world_map/coordinate_projection.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_world_map_exploration_state PRIVATE ${TEST_INCLUDE_DIRS})
|
||||||
|
target_include_directories(test_world_map_exploration_state SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(test_world_map_exploration_state PRIVATE catch2_main)
|
||||||
|
if(TARGET glm::glm)
|
||||||
|
target_link_libraries(test_world_map_exploration_state PRIVATE glm::glm)
|
||||||
|
endif()
|
||||||
|
add_test(NAME world_map_exploration_state COMMAND test_world_map_exploration_state)
|
||||||
|
register_test_target(test_world_map_exploration_state)
|
||||||
|
|
||||||
|
# ── test_world_map_zone_metadata ──────────────────────────────
|
||||||
|
add_executable(test_world_map_zone_metadata
|
||||||
|
test_world_map_zone_metadata.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/rendering/world_map/zone_metadata.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(test_world_map_zone_metadata PRIVATE ${TEST_INCLUDE_DIRS})
|
||||||
|
target_include_directories(test_world_map_zone_metadata SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(test_world_map_zone_metadata PRIVATE catch2_main)
|
||||||
|
if(TARGET glm::glm)
|
||||||
|
target_link_libraries(test_world_map_zone_metadata PRIVATE glm::glm)
|
||||||
|
endif()
|
||||||
|
add_test(NAME world_map_zone_metadata COMMAND test_world_map_zone_metadata)
|
||||||
|
register_test_target(test_world_map_zone_metadata)
|
||||||
|
|
||||||
# ── ASAN / UBSan for test targets ────────────────────────────
|
# ── ASAN / UBSan for test targets ────────────────────────────
|
||||||
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
|
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
|
||||||
foreach(_t IN LISTS ALL_TEST_TARGETS)
|
foreach(_t IN LISTS ALL_TEST_TARGETS)
|
||||||
|
|
|
||||||
498
tests/test_world_map.cpp
Normal file
498
tests/test_world_map.cpp
Normal file
|
|
@ -0,0 +1,498 @@
|
||||||
|
// Tests for WorldMap data structures and coordinate math
|
||||||
|
// Updated to use new modular types from world_map_types.hpp
|
||||||
|
#include <catch_amalgamated.hpp>
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using wowee::rendering::world_map::Zone;
|
||||||
|
using wowee::rendering::world_map::OverlayEntry;
|
||||||
|
using wowee::rendering::world_map::POI;
|
||||||
|
|
||||||
|
// ── MapPOI struct ────────────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("POI default-constructed is zeroed", "[world_map]") {
|
||||||
|
POI poi{};
|
||||||
|
REQUIRE(poi.id == 0);
|
||||||
|
REQUIRE(poi.importance == 0);
|
||||||
|
REQUIRE(poi.iconType == 0);
|
||||||
|
REQUIRE(poi.factionId == 0);
|
||||||
|
REQUIRE(poi.wowX == 0.0f);
|
||||||
|
REQUIRE(poi.wowY == 0.0f);
|
||||||
|
REQUIRE(poi.wowZ == 0.0f);
|
||||||
|
REQUIRE(poi.mapId == 0);
|
||||||
|
REQUIRE(poi.name.empty());
|
||||||
|
REQUIRE(poi.description.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("POI sorts by importance ascending", "[world_map]") {
|
||||||
|
std::vector<POI> pois;
|
||||||
|
|
||||||
|
POI capital;
|
||||||
|
capital.id = 1;
|
||||||
|
capital.importance = 3;
|
||||||
|
capital.name = "Stormwind";
|
||||||
|
pois.push_back(capital);
|
||||||
|
|
||||||
|
POI town;
|
||||||
|
town.id = 2;
|
||||||
|
town.importance = 1;
|
||||||
|
town.name = "Goldshire";
|
||||||
|
pois.push_back(town);
|
||||||
|
|
||||||
|
POI minor;
|
||||||
|
minor.id = 3;
|
||||||
|
minor.importance = 0;
|
||||||
|
minor.name = "Mirror Lake";
|
||||||
|
pois.push_back(minor);
|
||||||
|
|
||||||
|
std::sort(pois.begin(), pois.end(), [](const POI& a, const POI& b) {
|
||||||
|
return a.importance < b.importance;
|
||||||
|
});
|
||||||
|
|
||||||
|
REQUIRE(pois[0].name == "Mirror Lake");
|
||||||
|
REQUIRE(pois[1].name == "Goldshire");
|
||||||
|
REQUIRE(pois[2].name == "Stormwind");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WorldMapZone struct ──────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("Zone default-constructed is valid", "[world_map]") {
|
||||||
|
Zone z{};
|
||||||
|
REQUIRE(z.wmaID == 0);
|
||||||
|
REQUIRE(z.areaID == 0);
|
||||||
|
REQUIRE(z.areaName.empty());
|
||||||
|
REQUIRE(z.bounds.locLeft == 0.0f);
|
||||||
|
REQUIRE(z.bounds.locRight == 0.0f);
|
||||||
|
REQUIRE(z.bounds.locTop == 0.0f);
|
||||||
|
REQUIRE(z.bounds.locBottom == 0.0f);
|
||||||
|
REQUIRE(z.displayMapID == 0);
|
||||||
|
REQUIRE(z.parentWorldMapID == 0);
|
||||||
|
REQUIRE(z.exploreBits.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Zone areaID==0 identifies continent", "[world_map]") {
|
||||||
|
Zone continent{};
|
||||||
|
continent.areaID = 0;
|
||||||
|
continent.wmaID = 10;
|
||||||
|
continent.areaName = "Kalimdor";
|
||||||
|
|
||||||
|
Zone zone{};
|
||||||
|
zone.areaID = 440;
|
||||||
|
zone.wmaID = 100;
|
||||||
|
zone.areaName = "Tanaris";
|
||||||
|
|
||||||
|
REQUIRE(continent.areaID == 0);
|
||||||
|
REQUIRE(zone.areaID != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Coordinate projection logic ──────────────────────────────
|
||||||
|
// Replicate the UV projection formula from renderPosToMapUV for standalone testing.
|
||||||
|
|
||||||
|
static glm::vec2 computeMapUV(float wowX, float wowY,
|
||||||
|
float locLeft, float locRight,
|
||||||
|
float locTop, float locBottom,
|
||||||
|
bool isContinent) {
|
||||||
|
float denom_h = locLeft - locRight;
|
||||||
|
float denom_v = locTop - locBottom;
|
||||||
|
if (std::abs(denom_h) < 0.001f || std::abs(denom_v) < 0.001f)
|
||||||
|
return glm::vec2(0.5f, 0.5f);
|
||||||
|
|
||||||
|
float u = (locLeft - wowX) / denom_h;
|
||||||
|
float v = (locTop - wowY) / denom_v;
|
||||||
|
|
||||||
|
if (isContinent) {
|
||||||
|
constexpr float kVOffset = -0.15f;
|
||||||
|
v = (v - 0.5f) + 0.5f + kVOffset;
|
||||||
|
}
|
||||||
|
return glm::vec2(u, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("UV projection: center of zone maps to (0.5, 0.5)", "[world_map]") {
|
||||||
|
// Zone bounds: left=1000, right=0, top=1000, bottom=0
|
||||||
|
float centerX = 500.0f, centerY = 500.0f;
|
||||||
|
glm::vec2 uv = computeMapUV(centerX, centerY, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||||
|
REQUIRE(uv.x == Catch::Approx(0.5f).margin(0.001f));
|
||||||
|
REQUIRE(uv.y == Catch::Approx(0.5f).margin(0.001f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("UV projection: top-left corner maps to (0, 0)", "[world_map]") {
|
||||||
|
glm::vec2 uv = computeMapUV(1000.0f, 1000.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||||
|
REQUIRE(uv.x == Catch::Approx(0.0f).margin(0.001f));
|
||||||
|
REQUIRE(uv.y == Catch::Approx(0.0f).margin(0.001f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("UV projection: bottom-right corner maps to (1, 1)", "[world_map]") {
|
||||||
|
glm::vec2 uv = computeMapUV(0.0f, 0.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||||
|
REQUIRE(uv.x == Catch::Approx(1.0f).margin(0.001f));
|
||||||
|
REQUIRE(uv.y == Catch::Approx(1.0f).margin(0.001f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("UV projection: degenerate bounds returns center", "[world_map]") {
|
||||||
|
// left == right → degenerate
|
||||||
|
glm::vec2 uv = computeMapUV(500.0f, 500.0f, 500.0f, 500.0f, 1000.0f, 0.0f, false);
|
||||||
|
REQUIRE(uv.x == Catch::Approx(0.5f));
|
||||||
|
REQUIRE(uv.y == Catch::Approx(0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("UV projection: continent mode applies vertical offset", "[world_map]") {
|
||||||
|
// Same center point, but continent mode shifts V by kVOffset=-0.15
|
||||||
|
glm::vec2 uvZone = computeMapUV(500.0f, 500.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||||
|
glm::vec2 uvCont = computeMapUV(500.0f, 500.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, true);
|
||||||
|
|
||||||
|
REQUIRE(uvZone.x == Catch::Approx(uvCont.x).margin(0.001f));
|
||||||
|
// Continent V should be shifted by -0.15
|
||||||
|
REQUIRE(uvCont.y == Catch::Approx(uvZone.y - 0.15f).margin(0.001f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Expansion level derivation ───────────────────────────────
|
||||||
|
// Replicate the expansion detection logic from getExpansionLevel.
|
||||||
|
|
||||||
|
static int deriveExpansionLevel(int maxLevel) {
|
||||||
|
if (maxLevel <= 60) return 0; // vanilla
|
||||||
|
if (maxLevel <= 70) return 1; // TBC
|
||||||
|
return 2; // WotLK
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Expansion level from maxLevel", "[world_map]") {
|
||||||
|
REQUIRE(deriveExpansionLevel(60) == 0); // vanilla
|
||||||
|
REQUIRE(deriveExpansionLevel(58) == 0); // below vanilla cap
|
||||||
|
REQUIRE(deriveExpansionLevel(70) == 1); // TBC
|
||||||
|
REQUIRE(deriveExpansionLevel(65) == 1); // mid TBC range
|
||||||
|
REQUIRE(deriveExpansionLevel(80) == 2); // WotLK
|
||||||
|
REQUIRE(deriveExpansionLevel(75) == 2); // mid WotLK range
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Expansion continent filtering ────────────────────────────
|
||||||
|
|
||||||
|
static std::vector<uint32_t> filterContinentsByExpansion(
|
||||||
|
const std::vector<uint32_t>& mapIds, int expansionLevel) {
|
||||||
|
std::vector<uint32_t> result;
|
||||||
|
for (uint32_t id : mapIds) {
|
||||||
|
if (id == 530 && expansionLevel < 1) continue;
|
||||||
|
if (id == 571 && expansionLevel < 2) continue;
|
||||||
|
result.push_back(id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Vanilla hides TBC and WotLK continents", "[world_map]") {
|
||||||
|
std::vector<uint32_t> all = {0, 1, 530, 571};
|
||||||
|
auto filtered = filterContinentsByExpansion(all, 0);
|
||||||
|
REQUIRE(filtered.size() == 2);
|
||||||
|
REQUIRE(filtered[0] == 0);
|
||||||
|
REQUIRE(filtered[1] == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TBC shows Outland but hides Northrend", "[world_map]") {
|
||||||
|
std::vector<uint32_t> all = {0, 1, 530, 571};
|
||||||
|
auto filtered = filterContinentsByExpansion(all, 1);
|
||||||
|
REQUIRE(filtered.size() == 3);
|
||||||
|
REQUIRE(filtered[2] == 530);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WotLK shows all continents", "[world_map]") {
|
||||||
|
std::vector<uint32_t> all = {0, 1, 530, 571};
|
||||||
|
auto filtered = filterContinentsByExpansion(all, 2);
|
||||||
|
REQUIRE(filtered.size() == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POI faction coloring logic ───────────────────────────────
|
||||||
|
|
||||||
|
enum class Faction { Alliance, Horde, Neutral };
|
||||||
|
|
||||||
|
static Faction classifyFaction(uint32_t factionId) {
|
||||||
|
if (factionId == 469) return Faction::Alliance;
|
||||||
|
if (factionId == 67) return Faction::Horde;
|
||||||
|
return Faction::Neutral;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("POI faction classification", "[world_map]") {
|
||||||
|
REQUIRE(classifyFaction(469) == Faction::Alliance);
|
||||||
|
REQUIRE(classifyFaction(67) == Faction::Horde);
|
||||||
|
REQUIRE(classifyFaction(0) == Faction::Neutral);
|
||||||
|
REQUIRE(classifyFaction(35) == Faction::Neutral);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Overlay entry defaults ───────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("OverlayEntry defaults", "[world_map]") {
|
||||||
|
OverlayEntry ov{};
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
REQUIRE(ov.areaIDs[i] == 0);
|
||||||
|
}
|
||||||
|
REQUIRE(ov.textureName.empty());
|
||||||
|
REQUIRE(ov.texWidth == 0);
|
||||||
|
REQUIRE(ov.texHeight == 0);
|
||||||
|
REQUIRE(ov.offsetX == 0);
|
||||||
|
REQUIRE(ov.offsetY == 0);
|
||||||
|
REQUIRE(ov.hitRectLeft == 0);
|
||||||
|
REQUIRE(ov.hitRectRight == 0);
|
||||||
|
REQUIRE(ov.hitRectTop == 0);
|
||||||
|
REQUIRE(ov.hitRectBottom == 0);
|
||||||
|
REQUIRE(ov.tileCols == 0);
|
||||||
|
REQUIRE(ov.tileRows == 0);
|
||||||
|
REQUIRE(ov.tilesLoaded == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ZMP pixel-map zone lookup ────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("ZMP grid lookup resolves mouse UV to zone", "[world_map]") {
|
||||||
|
// Simulate a 128x128 ZMP grid with a zone at a known cell
|
||||||
|
std::array<uint32_t, 128 * 128> grid{};
|
||||||
|
uint32_t testAreaId = 42;
|
||||||
|
// Place area ID at grid cell (64, 64) — center of map
|
||||||
|
grid[64 * 128 + 64] = testAreaId;
|
||||||
|
|
||||||
|
// Mouse at UV (0.5, 0.5) → col=64, row=64
|
||||||
|
float mu = 0.5f, mv = 0.5f;
|
||||||
|
constexpr int ZMP_SIZE = 128;
|
||||||
|
int col = std::clamp(static_cast<int>(mu * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||||
|
int row = std::clamp(static_cast<int>(mv * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||||
|
uint32_t areaId = grid[row * ZMP_SIZE + col];
|
||||||
|
REQUIRE(areaId == testAreaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZMP grid returns 0 for empty cells", "[world_map]") {
|
||||||
|
std::array<uint32_t, 128 * 128> grid{};
|
||||||
|
// Empty grid — all cells zero (ocean/no zone)
|
||||||
|
constexpr int ZMP_SIZE = 128;
|
||||||
|
int col = 10, row = 10;
|
||||||
|
REQUIRE(grid[row * ZMP_SIZE + col] == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZMP grid clamps out-of-range UV", "[world_map]") {
|
||||||
|
std::array<uint32_t, 128 * 128> grid{};
|
||||||
|
grid[0] = 100; // (0,0) cell
|
||||||
|
grid[127 * 128 + 127] = 200; // (127,127) cell
|
||||||
|
|
||||||
|
constexpr int ZMP_SIZE = 128;
|
||||||
|
// UV at (-0.1, -0.1) should clamp to (0, 0)
|
||||||
|
float mu = -0.1f, mv = -0.1f;
|
||||||
|
int col = std::clamp(static_cast<int>(mu * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||||
|
int row = std::clamp(static_cast<int>(mv * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||||
|
REQUIRE(grid[row * ZMP_SIZE + col] == 100);
|
||||||
|
|
||||||
|
// UV at (1.5, 1.5) should clamp to (127, 127)
|
||||||
|
mu = 1.5f; mv = 1.5f;
|
||||||
|
col = std::clamp(static_cast<int>(mu * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||||
|
row = std::clamp(static_cast<int>(mv * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||||
|
REQUIRE(grid[row * ZMP_SIZE + col] == 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HitRect overlay AABB pre-filter ──────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("HitRect filters overlays correctly", "[world_map]") {
|
||||||
|
OverlayEntry ov{};
|
||||||
|
ov.hitRectLeft = 100;
|
||||||
|
ov.hitRectRight = 300;
|
||||||
|
ov.hitRectTop = 50;
|
||||||
|
ov.hitRectBottom = 200;
|
||||||
|
ov.texWidth = 200;
|
||||||
|
ov.texHeight = 150;
|
||||||
|
ov.textureName = "Goldshire";
|
||||||
|
|
||||||
|
bool hasHitRect = (ov.hitRectRight > ov.hitRectLeft &&
|
||||||
|
ov.hitRectBottom > ov.hitRectTop);
|
||||||
|
REQUIRE(hasHitRect);
|
||||||
|
|
||||||
|
// Point inside HitRect
|
||||||
|
float px = 150.0f, py = 100.0f;
|
||||||
|
bool inside = (px >= ov.hitRectLeft && px <= ov.hitRectRight &&
|
||||||
|
py >= ov.hitRectTop && py <= ov.hitRectBottom);
|
||||||
|
REQUIRE(inside);
|
||||||
|
|
||||||
|
// Point outside HitRect
|
||||||
|
px = 50.0f; py = 25.0f;
|
||||||
|
inside = (px >= ov.hitRectLeft && px <= ov.hitRectRight &&
|
||||||
|
py >= ov.hitRectTop && py <= ov.hitRectBottom);
|
||||||
|
REQUIRE_FALSE(inside);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("HitRect with zero values falls back to offset AABB", "[world_map]") {
|
||||||
|
OverlayEntry ov{};
|
||||||
|
// HitRect fields all zero → hasHitRect should be false
|
||||||
|
bool hasHitRect = (ov.hitRectRight > ov.hitRectLeft &&
|
||||||
|
ov.hitRectBottom > ov.hitRectTop);
|
||||||
|
REQUIRE_FALSE(hasHitRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Subzone hover with HitRect picks smallest overlay", "[world_map]") {
|
||||||
|
// Simulate two overlays — one large with HitRect, one small with HitRect
|
||||||
|
struct TestHitOverlay {
|
||||||
|
float hitLeft, hitRight, hitTop, hitBottom;
|
||||||
|
float texW, texH;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
|
||||||
|
TestHitOverlay large{0.0f, 500.0f, 0.0f, 400.0f, 500.0f, 400.0f, "BigArea"};
|
||||||
|
TestHitOverlay small{100.0f, 250.0f, 80.0f, 180.0f, 150.0f, 100.0f, "SmallArea"};
|
||||||
|
std::vector<TestHitOverlay> overlays = {large, small};
|
||||||
|
|
||||||
|
float px = 150.0f, py = 120.0f; // Inside both HitRects
|
||||||
|
std::string best;
|
||||||
|
float bestArea = std::numeric_limits<float>::max();
|
||||||
|
for (const auto& ov : overlays) {
|
||||||
|
bool inside = (px >= ov.hitLeft && px <= ov.hitRight &&
|
||||||
|
py >= ov.hitTop && py <= ov.hitBottom);
|
||||||
|
if (inside) {
|
||||||
|
float area = ov.texW * ov.texH;
|
||||||
|
if (area < bestArea) {
|
||||||
|
bestArea = area;
|
||||||
|
best = ov.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(best == "SmallArea");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cosmic view expansion logic ──────────────────────────────
|
||||||
|
|
||||||
|
struct CosmicMapEntry {
|
||||||
|
int mapId = 0;
|
||||||
|
std::string label;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<CosmicMapEntry> buildCosmicMaps(int expLevel) {
|
||||||
|
std::vector<CosmicMapEntry> maps;
|
||||||
|
if (expLevel == 0) return maps; // Vanilla: no cosmic
|
||||||
|
maps.push_back({0, "Azeroth"});
|
||||||
|
if (expLevel >= 1) maps.push_back({530, "Outland"});
|
||||||
|
if (expLevel >= 2) maps.push_back({571, "Northrend"});
|
||||||
|
return maps;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Vanilla has no cosmic view entries", "[world_map]") {
|
||||||
|
auto maps = buildCosmicMaps(0);
|
||||||
|
REQUIRE(maps.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TBC cosmic view has Azeroth + Outland", "[world_map]") {
|
||||||
|
auto maps = buildCosmicMaps(1);
|
||||||
|
REQUIRE(maps.size() == 2);
|
||||||
|
REQUIRE(maps[0].mapId == 0);
|
||||||
|
REQUIRE(maps[0].label == "Azeroth");
|
||||||
|
REQUIRE(maps[1].mapId == 530);
|
||||||
|
REQUIRE(maps[1].label == "Outland");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WotLK cosmic view has all three worlds", "[world_map]") {
|
||||||
|
auto maps = buildCosmicMaps(2);
|
||||||
|
REQUIRE(maps.size() == 3);
|
||||||
|
REQUIRE(maps[2].mapId == 571);
|
||||||
|
REQUIRE(maps[2].label == "Northrend");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subzone hover priority (smallest overlay wins) ───────────
|
||||||
|
|
||||||
|
struct TestOverlay {
|
||||||
|
float offsetX, offsetY;
|
||||||
|
float width, height;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string findSmallestOverlay(const std::vector<TestOverlay>& overlays,
|
||||||
|
float mu, float mv, float fboW, float fboH) {
|
||||||
|
std::string best;
|
||||||
|
float bestArea = std::numeric_limits<float>::max();
|
||||||
|
for (const auto& ov : overlays) {
|
||||||
|
float ovLeft = ov.offsetX / fboW;
|
||||||
|
float ovTop = ov.offsetY / fboH;
|
||||||
|
float ovRight = (ov.offsetX + ov.width) / fboW;
|
||||||
|
float ovBottom = (ov.offsetY + ov.height) / fboH;
|
||||||
|
if (mu >= ovLeft && mu <= ovRight && mv >= ovTop && mv <= ovBottom) {
|
||||||
|
float area = ov.width * ov.height;
|
||||||
|
if (area < bestArea) {
|
||||||
|
bestArea = area;
|
||||||
|
best = ov.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Subzone hover returns smallest overlapping overlay", "[world_map]") {
|
||||||
|
// Large overlay covers 0-512, 0-512
|
||||||
|
TestOverlay large{0.0f, 0.0f, 512.0f, 512.0f, "BigZone"};
|
||||||
|
// Small overlay covers 100-200, 100-200
|
||||||
|
TestOverlay small{100.0f, 100.0f, 100.0f, 100.0f, "SmallSubzone"};
|
||||||
|
|
||||||
|
std::vector<TestOverlay> overlays = {large, small};
|
||||||
|
|
||||||
|
// Mouse at UV (0.15, 0.15) → pixel (153.6, 115.2) for 1024x768 FBO
|
||||||
|
// Both overlays overlap; small should win
|
||||||
|
std::string result = findSmallestOverlay(overlays, 0.15f, 0.15f, 1024.0f, 768.0f);
|
||||||
|
REQUIRE(result == "SmallSubzone");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Subzone hover returns only overlay when one matches", "[world_map]") {
|
||||||
|
TestOverlay large{0.0f, 0.0f, 512.0f, 512.0f, "BigZone"};
|
||||||
|
TestOverlay small{100.0f, 100.0f, 100.0f, 100.0f, "SmallSubzone"};
|
||||||
|
std::vector<TestOverlay> overlays = {large, small};
|
||||||
|
|
||||||
|
// Mouse at UV (0.01, 0.01) → only large matches
|
||||||
|
std::string result = findSmallestOverlay(overlays, 0.01f, 0.01f, 1024.0f, 768.0f);
|
||||||
|
REQUIRE(result == "BigZone");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Subzone hover returns empty when nothing matches", "[world_map]") {
|
||||||
|
TestOverlay ov{100.0f, 100.0f, 50.0f, 50.0f, "Tiny"};
|
||||||
|
std::vector<TestOverlay> overlays = {ov};
|
||||||
|
|
||||||
|
std::string result = findSmallestOverlay(overlays, 0.01f, 0.01f, 1024.0f, 768.0f);
|
||||||
|
REQUIRE(result.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Zone metadata (level range + faction) ────────────────────
|
||||||
|
|
||||||
|
enum class TestFaction { Neutral, Alliance, Horde, Contested };
|
||||||
|
|
||||||
|
struct TestZoneMeta {
|
||||||
|
uint8_t minLevel = 0, maxLevel = 0;
|
||||||
|
TestFaction faction = TestFaction::Neutral;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string formatZoneLabel(const std::string& name, const TestZoneMeta* meta) {
|
||||||
|
std::string label = name;
|
||||||
|
if (meta) {
|
||||||
|
if (meta->minLevel > 0 && meta->maxLevel > 0) {
|
||||||
|
label += " (" + std::to_string(meta->minLevel) + "-" +
|
||||||
|
std::to_string(meta->maxLevel) + ")";
|
||||||
|
}
|
||||||
|
switch (meta->faction) {
|
||||||
|
case TestFaction::Alliance: label += " [Alliance]"; break;
|
||||||
|
case TestFaction::Horde: label += " [Horde]"; break;
|
||||||
|
case TestFaction::Contested: label += " [Contested]"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Zone label includes level range and faction", "[world_map]") {
|
||||||
|
TestZoneMeta meta{1, 10, TestFaction::Alliance};
|
||||||
|
std::string label = formatZoneLabel("Elwynn", &meta);
|
||||||
|
REQUIRE(label == "Elwynn (1-10) [Alliance]");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Zone label shows Contested faction", "[world_map]") {
|
||||||
|
TestZoneMeta meta{30, 45, TestFaction::Contested};
|
||||||
|
std::string label = formatZoneLabel("Stranglethorn", &meta);
|
||||||
|
REQUIRE(label == "Stranglethorn (30-45) [Contested]");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Zone label without metadata is just the name", "[world_map]") {
|
||||||
|
std::string label = formatZoneLabel("UnknownZone", nullptr);
|
||||||
|
REQUIRE(label == "UnknownZone");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Zone label with Neutral faction omits tag", "[world_map]") {
|
||||||
|
TestZoneMeta meta{55, 60, TestFaction::Neutral};
|
||||||
|
std::string label = formatZoneLabel("Moonglade", &meta);
|
||||||
|
REQUIRE(label == "Moonglade (55-60)");
|
||||||
|
}
|
||||||
193
tests/test_world_map_coordinate_projection.cpp
Normal file
193
tests/test_world_map_coordinate_projection.cpp
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
// Tests for the extracted world map coordinate projection module
|
||||||
|
#include <catch_amalgamated.hpp>
|
||||||
|
#include "rendering/world_map/coordinate_projection.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace wowee::rendering::world_map;
|
||||||
|
|
||||||
|
// ── Helper: build a minimal zone for testing ─────────────────
|
||||||
|
|
||||||
|
static Zone makeZone(uint32_t wmaID, uint32_t areaID,
|
||||||
|
float locLeft, float locRight,
|
||||||
|
float locTop, float locBottom,
|
||||||
|
uint32_t displayMapID = 0,
|
||||||
|
uint32_t parentWorldMapID = 0,
|
||||||
|
const std::string& name = "") {
|
||||||
|
Zone z;
|
||||||
|
z.wmaID = wmaID;
|
||||||
|
z.areaID = areaID;
|
||||||
|
z.areaName = name;
|
||||||
|
z.bounds.locLeft = locLeft;
|
||||||
|
z.bounds.locRight = locRight;
|
||||||
|
z.bounds.locTop = locTop;
|
||||||
|
z.bounds.locBottom = locBottom;
|
||||||
|
z.displayMapID = displayMapID;
|
||||||
|
z.parentWorldMapID = parentWorldMapID;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── renderPosToMapUV ─────────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("renderPosToMapUV: center of zone maps to (0.5, ~0.5)", "[world_map][coordinate_projection]") {
|
||||||
|
ZoneBounds bounds;
|
||||||
|
bounds.locLeft = 1000.0f;
|
||||||
|
bounds.locRight = -1000.0f;
|
||||||
|
bounds.locTop = 1000.0f;
|
||||||
|
bounds.locBottom = -1000.0f;
|
||||||
|
|
||||||
|
// renderPos.y = wowX, renderPos.x = wowY
|
||||||
|
glm::vec3 center(0.0f, 0.0f, 0.0f);
|
||||||
|
glm::vec2 uv = renderPosToMapUV(center, bounds, /*isContinent=*/false);
|
||||||
|
REQUIRE(std::abs(uv.x - 0.5f) < 0.01f);
|
||||||
|
REQUIRE(std::abs(uv.y - 0.5f) < 0.01f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("renderPosToMapUV: degenerate bounds returns (0.5, 0.5)", "[world_map][coordinate_projection]") {
|
||||||
|
ZoneBounds bounds{}; // all zeros
|
||||||
|
glm::vec3 pos(100.0f, 200.0f, 0.0f);
|
||||||
|
glm::vec2 uv = renderPosToMapUV(pos, bounds, false);
|
||||||
|
REQUIRE(uv.x == Catch::Approx(0.5f));
|
||||||
|
REQUIRE(uv.y == Catch::Approx(0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("renderPosToMapUV: top-left corner maps to (0, ~0)", "[world_map][coordinate_projection]") {
|
||||||
|
ZoneBounds bounds;
|
||||||
|
bounds.locLeft = 1000.0f;
|
||||||
|
bounds.locRight = -1000.0f;
|
||||||
|
bounds.locTop = 1000.0f;
|
||||||
|
bounds.locBottom = -1000.0f;
|
||||||
|
|
||||||
|
// wowX = renderPos.y = locLeft = 1000, wowY = renderPos.x = locTop = 1000
|
||||||
|
glm::vec3 topLeft(1000.0f, 1000.0f, 0.0f);
|
||||||
|
glm::vec2 uv = renderPosToMapUV(topLeft, bounds, false);
|
||||||
|
REQUIRE(uv.x == Catch::Approx(0.0f).margin(0.01f));
|
||||||
|
REQUIRE(uv.y == Catch::Approx(0.0f).margin(0.01f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("renderPosToMapUV: continent applies vertical offset", "[world_map][coordinate_projection]") {
|
||||||
|
ZoneBounds bounds;
|
||||||
|
bounds.locLeft = 1000.0f;
|
||||||
|
bounds.locRight = -1000.0f;
|
||||||
|
bounds.locTop = 1000.0f;
|
||||||
|
bounds.locBottom = -1000.0f;
|
||||||
|
|
||||||
|
glm::vec3 center(0.0f, 0.0f, 0.0f);
|
||||||
|
glm::vec2 zone_uv = renderPosToMapUV(center, bounds, false);
|
||||||
|
glm::vec2 cont_uv = renderPosToMapUV(center, bounds, true);
|
||||||
|
|
||||||
|
// Continent mode applies kVOffset = -0.15
|
||||||
|
REQUIRE(zone_uv.x == Catch::Approx(cont_uv.x).margin(0.01f));
|
||||||
|
REQUIRE(cont_uv.y != Catch::Approx(zone_uv.y).margin(0.01f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── zoneBelongsToContinent ───────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("zoneBelongsToContinent: parent match", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
// Continent at index 0
|
||||||
|
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "EK"));
|
||||||
|
// Zone at index 1: parentWorldMapID matches continent's wmaID
|
||||||
|
zones.push_back(makeZone(2, 100, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0, 1, "Elwynn"));
|
||||||
|
|
||||||
|
REQUIRE(zoneBelongsToContinent(zones, 1, 0) == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("zoneBelongsToContinent: no relation", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "EK"));
|
||||||
|
zones.push_back(makeZone(99, 100, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0, 50, "Far"));
|
||||||
|
|
||||||
|
REQUIRE(zoneBelongsToContinent(zones, 1, 0) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("zoneBelongsToContinent: out of bounds returns false", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f));
|
||||||
|
REQUIRE(zoneBelongsToContinent(zones, -1, 0) == false);
|
||||||
|
REQUIRE(zoneBelongsToContinent(zones, 5, 0) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── isRootContinent / isLeafContinent ────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("isRootContinent detects root with leaf children", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "Root"));
|
||||||
|
zones.push_back(makeZone(2, 0, 3000.0f, -3000.0f, 3000.0f, -3000.0f, 0, 1, "Leaf"));
|
||||||
|
|
||||||
|
REQUIRE(isRootContinent(zones, 0) == true);
|
||||||
|
REQUIRE(isLeafContinent(zones, 1) == true);
|
||||||
|
REQUIRE(isRootContinent(zones, 1) == false);
|
||||||
|
REQUIRE(isLeafContinent(zones, 0) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("isRootContinent: lone continent is not root (no children)", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "Solo"));
|
||||||
|
REQUIRE(isRootContinent(zones, 0) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("isRootContinent: out of bounds returns false", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
REQUIRE(isRootContinent(zones, 0) == false);
|
||||||
|
REQUIRE(isRootContinent(zones, -1) == false);
|
||||||
|
REQUIRE(isLeafContinent(zones, 0) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── findZoneForPlayer ────────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("findZoneForPlayer: finds smallest containing zone", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
// Continent (ignored: areaID == 0)
|
||||||
|
zones.push_back(makeZone(1, 0, 10000.0f, -10000.0f, 10000.0f, -10000.0f, 0, 0, "Cont"));
|
||||||
|
// Large zone
|
||||||
|
zones.push_back(makeZone(2, 100, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 1, "Large"));
|
||||||
|
// Small zone fully inside large
|
||||||
|
zones.push_back(makeZone(3, 200, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0, 1, "Small"));
|
||||||
|
|
||||||
|
// Player at center — should find the smaller zone
|
||||||
|
glm::vec3 playerPos(0.0f, 0.0f, 0.0f);
|
||||||
|
int found = findZoneForPlayer(zones, playerPos);
|
||||||
|
REQUIRE(found == 2); // Small zone
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findZoneForPlayer: returns -1 when no zone contains position", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 100, 100.0f, -100.0f, 100.0f, -100.0f, 0, 0, "Tiny"));
|
||||||
|
|
||||||
|
glm::vec3 farAway(9999.0f, 9999.0f, 0.0f);
|
||||||
|
REQUIRE(findZoneForPlayer(zones, farAway) == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── getContinentProjectionBounds ─────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("getContinentProjectionBounds: uses continent's own bounds if available", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 3000.0f, -3000.0f, 0, 0, "EK"));
|
||||||
|
|
||||||
|
float l, r, t, b;
|
||||||
|
bool ok = getContinentProjectionBounds(zones, 0, l, r, t, b);
|
||||||
|
REQUIRE(ok == true);
|
||||||
|
REQUIRE(l == Catch::Approx(5000.0f));
|
||||||
|
REQUIRE(r == Catch::Approx(-5000.0f));
|
||||||
|
REQUIRE(t == Catch::Approx(3000.0f));
|
||||||
|
REQUIRE(b == Catch::Approx(-3000.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("getContinentProjectionBounds: returns false for out of bounds", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
float l, r, t, b;
|
||||||
|
REQUIRE(getContinentProjectionBounds(zones, 0, l, r, t, b) == false);
|
||||||
|
REQUIRE(getContinentProjectionBounds(zones, -1, l, r, t, b) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("getContinentProjectionBounds: rejects non-continent zones", "[world_map][coordinate_projection]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 100, 5000.0f, -5000.0f, 3000.0f, -3000.0f, 0, 0, "Zone"));
|
||||||
|
float l, r, t, b;
|
||||||
|
bool ok = getContinentProjectionBounds(zones, 0, l, r, t, b);
|
||||||
|
REQUIRE(ok == false);
|
||||||
|
}
|
||||||
89
tests/test_world_map_exploration_state.cpp
Normal file
89
tests/test_world_map_exploration_state.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Tests for the extracted world map exploration state module
|
||||||
|
#include <catch_amalgamated.hpp>
|
||||||
|
#include "rendering/world_map/exploration_state.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
using namespace wowee::rendering::world_map;
|
||||||
|
|
||||||
|
static Zone makeZone(uint32_t wmaID, uint32_t areaID,
|
||||||
|
float locLeft, float locRight,
|
||||||
|
float locTop, float locBottom,
|
||||||
|
uint32_t parentWmaID = 0) {
|
||||||
|
Zone z;
|
||||||
|
z.wmaID = wmaID;
|
||||||
|
z.areaID = areaID;
|
||||||
|
z.bounds.locLeft = locLeft;
|
||||||
|
z.bounds.locRight = locRight;
|
||||||
|
z.bounds.locTop = locTop;
|
||||||
|
z.bounds.locBottom = locBottom;
|
||||||
|
z.parentWorldMapID = parentWmaID;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ExplorationState: initially has no server mask", "[world_map][exploration_state]") {
|
||||||
|
ExplorationState es;
|
||||||
|
REQUIRE(es.hasServerMask() == false);
|
||||||
|
REQUIRE(es.exploredZones().empty());
|
||||||
|
REQUIRE(es.exploredOverlays().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ExplorationState: setServerMask toggles hasServerMask", "[world_map][exploration_state]") {
|
||||||
|
ExplorationState es;
|
||||||
|
std::vector<uint32_t> mask = {0xFF, 0x00, 0x01};
|
||||||
|
es.setServerMask(mask, true);
|
||||||
|
REQUIRE(es.hasServerMask() == true);
|
||||||
|
|
||||||
|
es.setServerMask({}, false);
|
||||||
|
REQUIRE(es.hasServerMask() == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ExplorationState: overlaysChanged tracks changes", "[world_map][exploration_state]") {
|
||||||
|
ExplorationState es;
|
||||||
|
REQUIRE(es.overlaysChanged() == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ExplorationState: clearLocal resets local data", "[world_map][exploration_state]") {
|
||||||
|
ExplorationState es;
|
||||||
|
es.clearLocal();
|
||||||
|
REQUIRE(es.exploredZones().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ExplorationState: update with empty zones is safe", "[world_map][exploration_state]") {
|
||||||
|
ExplorationState es;
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||||
|
glm::vec3 pos(0.0f);
|
||||||
|
|
||||||
|
es.update(zones, pos, -1, exploreFlagByAreaId);
|
||||||
|
REQUIRE(es.exploredZones().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ExplorationState: update with valid zone and server mask", "[world_map][exploration_state]") {
|
||||||
|
ExplorationState es;
|
||||||
|
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
auto z = makeZone(1, 100, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0);
|
||||||
|
z.exploreBits.push_back(0); // bit 0
|
||||||
|
|
||||||
|
OverlayEntry ov;
|
||||||
|
ov.areaIDs[0] = 100;
|
||||||
|
z.overlays.push_back(ov);
|
||||||
|
zones.push_back(z);
|
||||||
|
|
||||||
|
// Set server mask with bit 0 set
|
||||||
|
es.setServerMask({0x01}, true);
|
||||||
|
|
||||||
|
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||||
|
exploreFlagByAreaId[100] = 0; // AreaID 100 → explore bit 0
|
||||||
|
|
||||||
|
glm::vec3 playerPos(0.0f, 0.0f, 0.0f);
|
||||||
|
es.update(zones, playerPos, 0, exploreFlagByAreaId);
|
||||||
|
|
||||||
|
// Zone should be explored since bit 0 is set in the mask
|
||||||
|
REQUIRE(es.exploredZones().count(0) == 1);
|
||||||
|
}
|
||||||
212
tests/test_world_map_map_resolver.cpp
Normal file
212
tests/test_world_map_map_resolver.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
// Tests for the map_resolver module — centralized map navigation resolution.
|
||||||
|
#include <catch_amalgamated.hpp>
|
||||||
|
#include "rendering/world_map/map_resolver.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
|
||||||
|
using namespace wowee::rendering::world_map;
|
||||||
|
|
||||||
|
// ── Helper: build minimal zones for testing ──────────────────
|
||||||
|
|
||||||
|
static Zone makeZone(uint32_t wmaID, uint32_t areaID,
|
||||||
|
const std::string& name = "",
|
||||||
|
uint32_t displayMapID = 0,
|
||||||
|
uint32_t parentWorldMapID = 0) {
|
||||||
|
Zone z;
|
||||||
|
z.wmaID = wmaID;
|
||||||
|
z.areaID = areaID;
|
||||||
|
z.areaName = name;
|
||||||
|
z.displayMapID = displayMapID;
|
||||||
|
z.parentWorldMapID = parentWorldMapID;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build zone list mimicking Azeroth (mapID=0) with root + leaf continents
|
||||||
|
static std::vector<Zone> buildAzerothZones() {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
// [0] Root continent (areaID=0, has children → isRootContinent)
|
||||||
|
zones.push_back(makeZone(1, 0, "Azeroth", 0, 0));
|
||||||
|
// [1] Leaf continent for EK (areaID=0, parentWorldMapID=1 → child of root)
|
||||||
|
zones.push_back(makeZone(2, 0, "EasternKingdoms", 0, 1));
|
||||||
|
// [2] Leaf continent for Kalimdor (shouldn't exist on mapID=0, but for testing)
|
||||||
|
zones.push_back(makeZone(3, 0, "Kalimdor", 1, 1));
|
||||||
|
// [3] Regular zone
|
||||||
|
zones.push_back(makeZone(10, 40, "Westfall", 0, 2));
|
||||||
|
// [4] Regular zone
|
||||||
|
zones.push_back(makeZone(11, 44, "Redridge", 0, 2));
|
||||||
|
return zones;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build zone list with only one continent (no leaf/root distinction)
|
||||||
|
static std::vector<Zone> buildSimpleZones() {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
// [0] Single continent entry
|
||||||
|
zones.push_back(makeZone(1, 0, "Kalimdor", 1, 0));
|
||||||
|
// [1] Zone
|
||||||
|
zones.push_back(makeZone(10, 331, "Ashenvale", 1, 1));
|
||||||
|
// [2] Zone
|
||||||
|
zones.push_back(makeZone(11, 400, "ThousandNeedles", 1, 1));
|
||||||
|
return zones;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── mapIdToFolder / folderToMapId / mapDisplayName ───────────
|
||||||
|
|
||||||
|
TEST_CASE("mapIdToFolder: known continent IDs",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
REQUIRE(std::string(mapIdToFolder(0)) == "Azeroth");
|
||||||
|
REQUIRE(std::string(mapIdToFolder(1)) == "Kalimdor");
|
||||||
|
REQUIRE(std::string(mapIdToFolder(530)) == "Expansion01");
|
||||||
|
REQUIRE(std::string(mapIdToFolder(571)) == "Northrend");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("mapIdToFolder: special views",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
REQUIRE(std::string(mapIdToFolder(UINT32_MAX)) == "World");
|
||||||
|
REQUIRE(std::string(mapIdToFolder(UINT32_MAX - 1)) == "Cosmic");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("mapIdToFolder: unknown returns empty",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
REQUIRE(std::string(mapIdToFolder(9999)) == "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("folderToMapId: case-insensitive lookup",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
REQUIRE(folderToMapId("Azeroth") == 0);
|
||||||
|
REQUIRE(folderToMapId("azeroth") == 0);
|
||||||
|
REQUIRE(folderToMapId("KALIMDOR") == 1);
|
||||||
|
REQUIRE(folderToMapId("Northrend") == 571);
|
||||||
|
REQUIRE(folderToMapId("world") == static_cast<int>(UINT32_MAX));
|
||||||
|
REQUIRE(folderToMapId("unknown") == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("mapDisplayName: returns UI labels",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
REQUIRE(std::string(mapDisplayName(0)) == "Eastern Kingdoms");
|
||||||
|
REQUIRE(std::string(mapDisplayName(1)) == "Kalimdor");
|
||||||
|
REQUIRE(std::string(mapDisplayName(571)) == "Northrend");
|
||||||
|
REQUIRE(mapDisplayName(9999) == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── findContinentForMapId ────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("findContinentForMapId: prefers leaf continent with matching displayMapID",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
int idx = findContinentForMapId(zones, 0, -1);
|
||||||
|
REQUIRE(idx == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findContinentForMapId: finds leaf by displayMapID=1 for Kalimdor",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
int idx = findContinentForMapId(zones, 1, -1);
|
||||||
|
REQUIRE(idx == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findContinentForMapId: falls back to first non-root continent",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildSimpleZones();
|
||||||
|
int idx = findContinentForMapId(zones, 999, -1);
|
||||||
|
REQUIRE(idx == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findContinentForMapId: skips cosmic zone index",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
int idx = findContinentForMapId(zones, 0, 1);
|
||||||
|
REQUIRE(idx == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("findContinentForMapId: returns -1 for empty zones",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
std::vector<Zone> empty;
|
||||||
|
int idx = findContinentForMapId(empty, 0, -1);
|
||||||
|
REQUIRE(idx == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── resolveWorldRegionClick ──────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("resolveWorldRegionClick: same map returns NAVIGATE_CONTINENT",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
auto result = resolveWorldRegionClick(0, zones, 0, -1);
|
||||||
|
REQUIRE(result.action == MapResolveAction::NAVIGATE_CONTINENT);
|
||||||
|
REQUIRE(result.targetZoneIdx == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("resolveWorldRegionClick: different map returns LOAD_MAP",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
auto result = resolveWorldRegionClick(1, zones, 0, -1);
|
||||||
|
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||||
|
REQUIRE(result.targetMapName == "Kalimdor");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("resolveWorldRegionClick: Northrend from Azeroth returns LOAD_MAP",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
auto result = resolveWorldRegionClick(571, zones, 0, -1);
|
||||||
|
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||||
|
REQUIRE(result.targetMapName == "Northrend");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("resolveWorldRegionClick: unknown mapId returns NONE",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
auto result = resolveWorldRegionClick(9999, zones, 0, -1);
|
||||||
|
REQUIRE(result.action == MapResolveAction::NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── resolveZoneClick ─────────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("resolveZoneClick: normal zone returns ENTER_ZONE",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
auto result = resolveZoneClick(3, zones, 0);
|
||||||
|
REQUIRE(result.action == MapResolveAction::ENTER_ZONE);
|
||||||
|
REQUIRE(result.targetZoneIdx == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("resolveZoneClick: zone with different displayMapID returns LOAD_MAP",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
std::vector<Zone> zones;
|
||||||
|
zones.push_back(makeZone(1, 0, "Azeroth", 0, 0));
|
||||||
|
zones.push_back(makeZone(50, 100, "DarkPortal", 530, 1));
|
||||||
|
|
||||||
|
auto result = resolveZoneClick(1, zones, 0);
|
||||||
|
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||||
|
REQUIRE(result.targetMapName == "Expansion01");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("resolveZoneClick: zone with displayMapID matching current returns ENTER_ZONE",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildSimpleZones();
|
||||||
|
auto result = resolveZoneClick(1, zones, 1);
|
||||||
|
REQUIRE(result.action == MapResolveAction::ENTER_ZONE);
|
||||||
|
REQUIRE(result.targetZoneIdx == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("resolveZoneClick: out of range returns NONE",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto zones = buildAzerothZones();
|
||||||
|
auto result = resolveZoneClick(-1, zones, 0);
|
||||||
|
REQUIRE(result.action == MapResolveAction::NONE);
|
||||||
|
|
||||||
|
result = resolveZoneClick(99, zones, 0);
|
||||||
|
REQUIRE(result.action == MapResolveAction::NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── resolveCosmicClick ───────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("resolveCosmicClick: returns LOAD_MAP for known mapId",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto result = resolveCosmicClick(530);
|
||||||
|
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||||
|
REQUIRE(result.targetMapName == "Expansion01");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("resolveCosmicClick: returns NONE for unknown mapId",
|
||||||
|
"[world_map][map_resolver]") {
|
||||||
|
auto result = resolveCosmicClick(9999);
|
||||||
|
REQUIRE(result.action == MapResolveAction::NONE);
|
||||||
|
}
|
||||||
198
tests/test_world_map_view_state_machine.cpp
Normal file
198
tests/test_world_map_view_state_machine.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
// Tests for the extracted world map view state machine module
|
||||||
|
#include <catch_amalgamated.hpp>
|
||||||
|
#include "rendering/world_map/view_state_machine.hpp"
|
||||||
|
|
||||||
|
using namespace wowee::rendering::world_map;
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: initial state is CONTINENT", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::CONTINENT);
|
||||||
|
REQUIRE(sm.continentIdx() == -1);
|
||||||
|
REQUIRE(sm.currentZoneIdx() == -1);
|
||||||
|
REQUIRE(sm.transition().active == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomIn from CONTINENT to ZONE", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
|
||||||
|
auto result = sm.zoomIn(5, 5);
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::ZONE);
|
||||||
|
REQUIRE(result.targetIdx == 5);
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::ZONE);
|
||||||
|
REQUIRE(sm.currentZoneIdx() == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomIn from CONTINENT with no zone does nothing", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
|
||||||
|
auto result = sm.zoomIn(-1, -1);
|
||||||
|
REQUIRE(result.changed == false);
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::CONTINENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomOut from ZONE to CONTINENT", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::ZONE);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
sm.setCurrentZoneIdx(5);
|
||||||
|
|
||||||
|
auto result = sm.zoomOut();
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::CONTINENT);
|
||||||
|
REQUIRE(result.targetIdx == 0);
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::CONTINENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomOut from ZONE without continent does nothing", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::ZONE);
|
||||||
|
sm.setContinentIdx(-1);
|
||||||
|
|
||||||
|
auto result = sm.zoomOut();
|
||||||
|
REQUIRE(result.changed == false);
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::ZONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomOut from CONTINENT to WORLD", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
|
||||||
|
auto result = sm.zoomOut();
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::WORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomOut from WORLD to COSMIC when enabled", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::WORLD);
|
||||||
|
sm.setCosmicEnabled(true);
|
||||||
|
|
||||||
|
auto result = sm.zoomOut();
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::COSMIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomOut from WORLD stays when cosmic disabled", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::WORLD);
|
||||||
|
sm.setCosmicEnabled(false);
|
||||||
|
|
||||||
|
auto result = sm.zoomOut();
|
||||||
|
REQUIRE(result.changed == false);
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::WORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomIn from COSMIC goes to WORLD", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::COSMIC);
|
||||||
|
sm.setCosmicEnabled(true);
|
||||||
|
|
||||||
|
auto result = sm.zoomIn(-1, -1);
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomIn from WORLD to CONTINENT with continent set", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::WORLD);
|
||||||
|
sm.setContinentIdx(3);
|
||||||
|
|
||||||
|
auto result = sm.zoomIn(-1, -1);
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::CONTINENT);
|
||||||
|
REQUIRE(result.targetIdx == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: enterWorldView sets WORLD level", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::ZONE);
|
||||||
|
|
||||||
|
auto result = sm.enterWorldView();
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||||
|
REQUIRE(sm.currentLevel() == ViewLevel::WORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: enterCosmicView when disabled falls back to WORLD", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setCosmicEnabled(false);
|
||||||
|
|
||||||
|
auto result = sm.enterCosmicView();
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: enterZone goes to ZONE level", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
|
||||||
|
auto result = sm.enterZone(7);
|
||||||
|
REQUIRE(result.changed == true);
|
||||||
|
REQUIRE(result.newLevel == ViewLevel::ZONE);
|
||||||
|
REQUIRE(result.targetIdx == 7);
|
||||||
|
REQUIRE(sm.currentZoneIdx() == 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Transition animation ─────────────────────────────────────
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: transition starts on zoom", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
sm.zoomIn(5, 5);
|
||||||
|
|
||||||
|
REQUIRE(sm.transition().active == true);
|
||||||
|
REQUIRE(sm.transition().progress == Catch::Approx(0.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: updateTransition advances progress", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
sm.zoomIn(5, 5);
|
||||||
|
|
||||||
|
float halfDuration = sm.transition().duration / 2.0f;
|
||||||
|
bool stillActive = sm.updateTransition(halfDuration);
|
||||||
|
REQUIRE(stillActive == true);
|
||||||
|
REQUIRE(sm.transition().progress == Catch::Approx(0.5f).margin(0.01f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: transition completes after full duration", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
sm.zoomIn(5, 5);
|
||||||
|
|
||||||
|
float dur = sm.transition().duration;
|
||||||
|
sm.updateTransition(dur + 0.1f); // overshoot
|
||||||
|
REQUIRE(sm.transition().active == false);
|
||||||
|
REQUIRE(sm.transition().progress == Catch::Approx(1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: updateTransition when no transition returns false", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
REQUIRE(sm.updateTransition(0.1f) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomIn prefers hovered zone over player zone", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
|
||||||
|
auto result = sm.zoomIn(/*hovered=*/3, /*player=*/7);
|
||||||
|
REQUIRE(result.targetIdx == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ViewStateMachine: zoomIn falls back to player zone when no hover", "[world_map][view_state_machine]") {
|
||||||
|
ViewStateMachine sm;
|
||||||
|
sm.setLevel(ViewLevel::CONTINENT);
|
||||||
|
sm.setContinentIdx(0);
|
||||||
|
|
||||||
|
auto result = sm.zoomIn(/*hovered=*/-1, /*player=*/7);
|
||||||
|
REQUIRE(result.targetIdx == 7);
|
||||||
|
}
|
||||||
86
tests/test_world_map_zone_metadata.cpp
Normal file
86
tests/test_world_map_zone_metadata.cpp
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Tests for the extracted world map zone metadata module
|
||||||
|
#include <catch_amalgamated.hpp>
|
||||||
|
#include "rendering/world_map/zone_metadata.hpp"
|
||||||
|
#include "rendering/world_map/world_map_types.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace wowee::rendering::world_map;
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: find returns nullptr for unknown zone", "[world_map][zone_metadata]") {
|
||||||
|
ZoneMetadata zm;
|
||||||
|
zm.initialize();
|
||||||
|
REQUIRE(zm.find("NonexistentZoneXYZ") == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: find returns valid data for known zones", "[world_map][zone_metadata]") {
|
||||||
|
ZoneMetadata zm;
|
||||||
|
zm.initialize();
|
||||||
|
|
||||||
|
const ZoneMeta* elwynn = zm.find("Elwynn");
|
||||||
|
REQUIRE(elwynn != nullptr);
|
||||||
|
REQUIRE(elwynn->minLevel > 0);
|
||||||
|
REQUIRE(elwynn->maxLevel >= elwynn->minLevel);
|
||||||
|
REQUIRE(elwynn->faction == ZoneFaction::Alliance);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: Contested zones", "[world_map][zone_metadata]") {
|
||||||
|
ZoneMetadata zm;
|
||||||
|
zm.initialize();
|
||||||
|
|
||||||
|
const ZoneMeta* sTV = zm.find("StranglethornVale");
|
||||||
|
REQUIRE(sTV != nullptr);
|
||||||
|
REQUIRE(sTV->faction == ZoneFaction::Contested);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: Horde zones", "[world_map][zone_metadata]") {
|
||||||
|
ZoneMetadata zm;
|
||||||
|
zm.initialize();
|
||||||
|
|
||||||
|
const ZoneMeta* durotar = zm.find("Durotar");
|
||||||
|
REQUIRE(durotar != nullptr);
|
||||||
|
REQUIRE(durotar->faction == ZoneFaction::Horde);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: formatLabel with no metadata", "[world_map][zone_metadata]") {
|
||||||
|
std::string label = ZoneMetadata::formatLabel("UnknownZone", nullptr);
|
||||||
|
REQUIRE(label == "UnknownZone");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: formatLabel with metadata", "[world_map][zone_metadata]") {
|
||||||
|
ZoneMeta meta;
|
||||||
|
meta.minLevel = 10;
|
||||||
|
meta.maxLevel = 20;
|
||||||
|
meta.faction = ZoneFaction::Alliance;
|
||||||
|
|
||||||
|
std::string label = ZoneMetadata::formatLabel("Elwynn", &meta);
|
||||||
|
// Should contain the zone name
|
||||||
|
REQUIRE(label.find("Elwynn") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: formatHoverLabel with metadata", "[world_map][zone_metadata]") {
|
||||||
|
ZoneMeta meta;
|
||||||
|
meta.minLevel = 30;
|
||||||
|
meta.maxLevel = 40;
|
||||||
|
meta.faction = ZoneFaction::Contested;
|
||||||
|
|
||||||
|
std::string label = ZoneMetadata::formatHoverLabel("StranglethornVale", &meta);
|
||||||
|
// Should contain both zone name and level range
|
||||||
|
REQUIRE(label.find("StranglethornVale") != std::string::npos);
|
||||||
|
REQUIRE(label.find("30") != std::string::npos);
|
||||||
|
REQUIRE(label.find("40") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: formatHoverLabel with no metadata just returns name", "[world_map][zone_metadata]") {
|
||||||
|
std::string label = ZoneMetadata::formatHoverLabel("UnknownZone", nullptr);
|
||||||
|
REQUIRE(label == "UnknownZone");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ZoneMetadata: double initialization is safe", "[world_map][zone_metadata]") {
|
||||||
|
ZoneMetadata zm;
|
||||||
|
zm.initialize();
|
||||||
|
zm.initialize(); // should not crash or change data
|
||||||
|
|
||||||
|
const ZoneMeta* elwynn = zm.find("Elwynn");
|
||||||
|
REQUIRE(elwynn != nullptr);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue