refactor: decompose world map into modular component architecture

Break the monolithic 1360-line world_map.cpp into 16 focused modules
under src/rendering/world_map/:

Architecture:
- world_map_facade: public API composing all components (PIMPL)
- world_map_types: Vulkan-free domain types (Zone, ViewLevel, etc.)
- data_repository: DBC zone loading, ZMP pixel map, POI/overlay storage
- coordinate_projection: UV projection, zone/continent lookups
- composite_renderer: Vulkan tile pipeline + off-screen compositing
- exploration_state: server mask + local exploration tracking
- view_state_machine: COSMIC→WORLD→CONTINENT→ZONE navigation
- input_handler: keyboard/mouse input → InputAction mapping
- overlay_renderer: layer-based ImGui overlay system (OCP)
- map_resolver: cross-map navigation (Outland, Northrend, etc.)
- zone_metadata: level ranges and faction data

Overlay layers (each an IOverlayLayer):
- player_marker, party_dot, taxi_node, poi_marker, quest_poi,
  corpse_marker, zone_highlight, coordinate_display, subzone_tooltip

Fixes:
- Player marker no longer bleeds across continents (only shown when
  player is in a zone belonging to the displayed continent)
- Zone hover uses DBC-projected AABB rectangles (restored from
  original working behavior)
- Exploration overlay rendering for zone view subzones

Tests:
- 6 new test files covering coordinate projection, exploration state,
  map resolver, view state machine, zone metadata, and integration

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
Pavel Okhlopkov 2026-04-12 09:52:51 +03:00
parent db3f65a87e
commit fff06fc932
55 changed files with 6335 additions and 1542 deletions

View file

@ -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

View 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

View 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;
}
// ── Zonecontinent 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load diff

View 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

View file

@ -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());

View file

@ -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 {