mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-14 00:23:50 +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/quest_marker_renderer.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/mount_dust.cpp
|
||||
src/rendering/levelup_effect.cpp
|
||||
|
|
@ -778,6 +796,26 @@ set(WOWEE_HEADERS
|
|||
include/rendering/lightning.hpp
|
||||
include/rendering/swim_effects.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_preview.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
|
||||
static const char* mapIdToName(uint32_t mapId);
|
||||
static int mapNameToId(const std::string& name);
|
||||
static const char* mapDisplayName(uint32_t mapId);
|
||||
|
||||
// Background preloading — warms AssetManager file cache
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ class CharacterRenderer;
|
|||
class WMORenderer;
|
||||
class M2Renderer;
|
||||
class Minimap;
|
||||
class WorldMap;
|
||||
namespace world_map { class WorldMapFacade; }
|
||||
using WorldMap = world_map::WorldMapFacade;
|
||||
class QuestMarkerRenderer;
|
||||
class CharacterPreview;
|
||||
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
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include "rendering/world_map/world_map_facade.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace rendering {
|
||||
|
||||
class VkContext;
|
||||
class VkTexture;
|
||||
class VkRenderTarget;
|
||||
// Backward-compatible type aliases for old consumer code
|
||||
// (game_screen_hud.cpp, renderer.cpp, etc.)
|
||||
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.
|
||||
struct WorldMapPartyDot {
|
||||
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 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_;
|
||||
};
|
||||
// WorldMap alias is already provided by world_map_facade.hpp:
|
||||
// using WorldMap = world_map::WorldMapFacade;
|
||||
// WorldMap::QuestPoi alias is provided inside WorldMapFacade:
|
||||
// using QuestPoi = QuestPOI;
|
||||
|
||||
} // namespace rendering
|
||||
} // 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) {
|
||||
case 0: return "Eastern Kingdoms";
|
||||
case 1: return "Kalimdor";
|
||||
case 13: return "Test";
|
||||
case 169: return "Emerald Dream";
|
||||
case 530: return "Outland";
|
||||
case 571: return "Northrend";
|
||||
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() {
|
||||
if (!pendingWorldEntry_ || loadingWorld_) return;
|
||||
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/ui_colors.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/world_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
|
|
@ -611,13 +612,8 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
|
|||
}
|
||||
// Fall back to continent name if zone unavailable
|
||||
if (homeLocation.empty()) {
|
||||
switch (mapId) {
|
||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
||||
case 1: homeLocation = "Kalimdor"; break;
|
||||
case 530: homeLocation = "Outland"; break;
|
||||
case 571: homeLocation = "Northrend"; break;
|
||||
default: homeLocation = "Unknown"; break;
|
||||
}
|
||||
const char* dn = core::WorldLoader::mapDisplayName(mapId);
|
||||
homeLocation = dn ? dn : "Unknown";
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
||||
"Home: %s", homeLocation.c_str());
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "ui/keybinding_manager.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/world_loader.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
|
|
@ -2632,15 +2633,8 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
homeLocation = gameHandler_->getWhoAreaName(zoneId);
|
||||
// Fall back to continent name if zone unavailable
|
||||
if (homeLocation.empty()) {
|
||||
switch (mapId) {
|
||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
||||
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;
|
||||
}
|
||||
const char* dn = core::WorldLoader::mapDisplayName(mapId);
|
||||
homeLocation = dn ? dn : "Unknown";
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", homeLocation.c_str());
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -263,6 +263,92 @@ endif()
|
|||
add_test(NAME transport_components COMMAND 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 ────────────────────────────
|
||||
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
|
||||
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