mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-05 08:43:50 +00:00
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:
parent
db3f65a87e
commit
fff06fc932
55 changed files with 6335 additions and 1542 deletions
|
|
@ -71,6 +71,8 @@ const char* WorldLoader::mapDisplayName(uint32_t mapId) {
|
|||
switch (mapId) {
|
||||
case 0: return "Eastern Kingdoms";
|
||||
case 1: return "Kalimdor";
|
||||
case 13: return "Test";
|
||||
case 169: return "Emerald Dream";
|
||||
case 530: return "Outland";
|
||||
case 571: return "Northrend";
|
||||
default: return nullptr;
|
||||
|
|
@ -170,6 +172,15 @@ const char* WorldLoader::mapIdToName(uint32_t mapId) {
|
|||
}
|
||||
}
|
||||
|
||||
int WorldLoader::mapNameToId(const std::string& name) {
|
||||
// Reverse lookup: iterate known continent IDs and match against mapIdToName.
|
||||
static constexpr uint32_t kContinentIds[] = {0, 1, 530, 571};
|
||||
for (uint32_t id : kContinentIds) {
|
||||
if (name == mapIdToName(id)) return static_cast<int>(id);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void WorldLoader::processPendingEntry() {
|
||||
if (!pendingWorldEntry_ || loadingWorld_) return;
|
||||
auto entry = *pendingWorldEntry_;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
570
src/rendering/world_map/composite_renderer.cpp
Normal file
570
src/rendering/world_map/composite_renderer.cpp
Normal file
|
|
@ -0,0 +1,570 @@
|
|||
// composite_renderer.cpp — Vulkan off-screen composite rendering for the world map.
|
||||
// Extracted from WorldMap::initialize, shutdown, compositePass, loadZoneTextures,
|
||||
// loadOverlayTextures, destroyZoneTextures (Phase 7 of refactoring plan).
|
||||
#include "rendering/world_map/composite_renderer.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_render_target.hpp"
|
||||
#include "rendering/vk_pipeline.hpp"
|
||||
#include "rendering/vk_shader.hpp"
|
||||
#include "rendering/vk_utils.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
CompositeRenderer::CompositeRenderer() = default;
|
||||
|
||||
CompositeRenderer::~CompositeRenderer() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
void CompositeRenderer::ensureTextureSlots(size_t zoneCount, const std::vector<Zone>& zones) {
|
||||
if (zoneTextureSlots_.size() >= zoneCount) return;
|
||||
zoneTextureSlots_.resize(zoneCount);
|
||||
for (size_t i = 0; i < zoneCount; i++) {
|
||||
auto& slots = zoneTextureSlots_[i];
|
||||
if (slots.overlays.size() != zones[i].overlays.size()) {
|
||||
slots.overlays.resize(zones[i].overlays.size());
|
||||
for (size_t oi = 0; oi < zones[i].overlays.size(); oi++) {
|
||||
const auto& ov = zones[i].overlays[oi];
|
||||
slots.overlays[oi].tiles.resize(ov.tileCols * ov.tileRows, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CompositeRenderer::initialize(VkContext* ctx, pipeline::AssetManager* am) {
|
||||
if (initialized) return true;
|
||||
vkCtx = ctx;
|
||||
assetManager = am;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
|
||||
// --- Composite render target (1024x768) ---
|
||||
compositeTarget = std::make_unique<VkRenderTarget>();
|
||||
if (!compositeTarget->create(*vkCtx, FBO_W, FBO_H)) {
|
||||
LOG_ERROR("CompositeRenderer: failed to create composite render target");
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Quad vertex buffer (unit quad: pos2 + uv2) ---
|
||||
float quadVerts[] = {
|
||||
0.0f, 0.0f, 0.0f, 0.0f,
|
||||
1.0f, 0.0f, 1.0f, 0.0f,
|
||||
1.0f, 1.0f, 1.0f, 1.0f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f,
|
||||
1.0f, 1.0f, 1.0f, 1.0f,
|
||||
0.0f, 1.0f, 0.0f, 1.0f,
|
||||
};
|
||||
auto quadBuf = uploadBuffer(*vkCtx, quadVerts, sizeof(quadVerts),
|
||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||
quadVB = quadBuf.buffer;
|
||||
quadVBAlloc = quadBuf.allocation;
|
||||
|
||||
// --- Descriptor set layout: 1 combined image sampler at binding 0 ---
|
||||
VkDescriptorSetLayoutBinding samplerBinding{};
|
||||
samplerBinding.binding = 0;
|
||||
samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
samplerBinding.descriptorCount = 1;
|
||||
samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
samplerSetLayout = createDescriptorSetLayout(device, { samplerBinding });
|
||||
|
||||
// --- Descriptor pool ---
|
||||
VkDescriptorPoolSize poolSize{};
|
||||
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
poolSize.descriptorCount = MAX_DESC_SETS;
|
||||
|
||||
VkDescriptorPoolCreateInfo poolInfo{};
|
||||
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
poolInfo.maxSets = MAX_DESC_SETS;
|
||||
poolInfo.poolSizeCount = 1;
|
||||
poolInfo.pPoolSizes = &poolSize;
|
||||
vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool);
|
||||
|
||||
// --- Allocate descriptor sets ---
|
||||
constexpr uint32_t tileSetCount = 24;
|
||||
constexpr uint32_t overlaySetCount = MAX_OVERLAY_TILES * 2;
|
||||
constexpr uint32_t totalSets = tileSetCount + 1 + 1 + overlaySetCount;
|
||||
std::vector<VkDescriptorSetLayout> layouts(totalSets, samplerSetLayout);
|
||||
VkDescriptorSetAllocateInfo allocInfo{};
|
||||
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
allocInfo.descriptorPool = descPool;
|
||||
allocInfo.descriptorSetCount = totalSets;
|
||||
allocInfo.pSetLayouts = layouts.data();
|
||||
|
||||
std::vector<VkDescriptorSet> allSets(totalSets);
|
||||
vkAllocateDescriptorSets(device, &allocInfo, allSets.data());
|
||||
|
||||
uint32_t si = 0;
|
||||
for (int f = 0; f < 2; f++)
|
||||
for (int t = 0; t < 12; t++)
|
||||
tileDescSets[f][t] = allSets[si++];
|
||||
imguiDisplaySet = allSets[si++];
|
||||
fogDescSet_ = allSets[si++];
|
||||
for (int f = 0; f < 2; f++)
|
||||
for (uint32_t t = 0; t < MAX_OVERLAY_TILES; t++)
|
||||
overlayDescSets_[f][t] = allSets[si++];
|
||||
|
||||
// --- Write display descriptor set → composite render target ---
|
||||
VkDescriptorImageInfo compositeImgInfo = compositeTarget->descriptorInfo();
|
||||
VkWriteDescriptorSet displayWrite{};
|
||||
displayWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
displayWrite.dstSet = imguiDisplaySet;
|
||||
displayWrite.dstBinding = 0;
|
||||
displayWrite.descriptorCount = 1;
|
||||
displayWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
displayWrite.pImageInfo = &compositeImgInfo;
|
||||
vkUpdateDescriptorSets(device, 1, &displayWrite, 0, nullptr);
|
||||
|
||||
// --- Pipeline layout: samplerSetLayout + push constant (16 bytes, vertex) ---
|
||||
VkPushConstantRange tilePush{};
|
||||
tilePush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
tilePush.offset = 0;
|
||||
tilePush.size = sizeof(WorldMapTilePush);
|
||||
tilePipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { tilePush });
|
||||
|
||||
// --- Vertex input: pos2 (loc 0) + uv2 (loc 1), stride 16 ---
|
||||
VkVertexInputBindingDescription binding{};
|
||||
binding.binding = 0;
|
||||
binding.stride = 4 * sizeof(float);
|
||||
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||
|
||||
std::vector<VkVertexInputAttributeDescription> attrs(2);
|
||||
attrs[0] = { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 };
|
||||
attrs[1] = { 1, 0, VK_FORMAT_R32G32_SFLOAT, 2 * sizeof(float) };
|
||||
|
||||
// --- Load tile shaders and build pipeline ---
|
||||
{
|
||||
VkShaderModule vs, fs;
|
||||
if (!vs.loadFromFile(device, "assets/shaders/world_map.vert.spv") ||
|
||||
!fs.loadFromFile(device, "assets/shaders/world_map.frag.spv")) {
|
||||
LOG_ERROR("CompositeRenderer: failed to load tile shaders");
|
||||
return false;
|
||||
}
|
||||
|
||||
tilePipeline = PipelineBuilder()
|
||||
.setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({ binding }, attrs)
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
||||
.setLayout(tilePipelineLayout)
|
||||
.setRenderPass(compositeTarget->getRenderPass())
|
||||
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
||||
.build(device, vkCtx->getPipelineCache());
|
||||
|
||||
vs.destroy();
|
||||
fs.destroy();
|
||||
}
|
||||
|
||||
if (!tilePipeline) {
|
||||
LOG_ERROR("CompositeRenderer: failed to create tile pipeline");
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Overlay pipeline (alpha-blended) ---
|
||||
{
|
||||
VkPushConstantRange overlayPushVert{};
|
||||
overlayPushVert.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
overlayPushVert.offset = 0;
|
||||
overlayPushVert.size = sizeof(WorldMapTilePush);
|
||||
|
||||
VkPushConstantRange overlayPushFrag{};
|
||||
overlayPushFrag.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
overlayPushFrag.offset = 16;
|
||||
overlayPushFrag.size = sizeof(glm::vec4);
|
||||
|
||||
overlayPipelineLayout_ = createPipelineLayout(device, { samplerSetLayout },
|
||||
{ overlayPushVert, overlayPushFrag });
|
||||
|
||||
VkShaderModule vs, fs;
|
||||
if (!vs.loadFromFile(device, "assets/shaders/world_map.vert.spv") ||
|
||||
!fs.loadFromFile(device, "assets/shaders/world_map_fog.frag.spv")) {
|
||||
LOG_ERROR("CompositeRenderer: failed to load overlay shaders");
|
||||
return false;
|
||||
}
|
||||
|
||||
overlayPipeline_ = PipelineBuilder()
|
||||
.setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
||||
fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
||||
.setVertexInput({ binding }, attrs)
|
||||
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
||||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setNoDepthTest()
|
||||
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
||||
.setLayout(overlayPipelineLayout_)
|
||||
.setRenderPass(compositeTarget->getRenderPass())
|
||||
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
||||
.build(device, vkCtx->getPipelineCache());
|
||||
|
||||
vs.destroy();
|
||||
fs.destroy();
|
||||
}
|
||||
|
||||
if (!overlayPipeline_) {
|
||||
LOG_ERROR("CompositeRenderer: failed to create overlay pipeline");
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 1×1 white fog texture ---
|
||||
{
|
||||
uint8_t white[] = { 255, 255, 255, 255 };
|
||||
fogTexture_ = std::make_unique<VkTexture>();
|
||||
fogTexture_->upload(*vkCtx, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||
fogTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST,
|
||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
||||
|
||||
VkDescriptorImageInfo fogImgInfo = fogTexture_->descriptorInfo();
|
||||
VkWriteDescriptorSet fogWrite{};
|
||||
fogWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
fogWrite.dstSet = fogDescSet_;
|
||||
fogWrite.dstBinding = 0;
|
||||
fogWrite.descriptorCount = 1;
|
||||
fogWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
fogWrite.pImageInfo = &fogImgInfo;
|
||||
vkUpdateDescriptorSets(device, 1, &fogWrite, 0, nullptr);
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
LOG_INFO("CompositeRenderer initialized (", FBO_W, "x", FBO_H, " composite)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void CompositeRenderer::shutdown() {
|
||||
if (!vkCtx) return;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
|
||||
vkDeviceWaitIdle(device);
|
||||
|
||||
if (tilePipeline) { vkDestroyPipeline(device, tilePipeline, nullptr); tilePipeline = VK_NULL_HANDLE; }
|
||||
if (tilePipelineLayout) { vkDestroyPipelineLayout(device, tilePipelineLayout, nullptr); tilePipelineLayout = VK_NULL_HANDLE; }
|
||||
if (overlayPipeline_) { vkDestroyPipeline(device, overlayPipeline_, nullptr); overlayPipeline_ = VK_NULL_HANDLE; }
|
||||
if (overlayPipelineLayout_) { vkDestroyPipelineLayout(device, overlayPipelineLayout_, nullptr); overlayPipelineLayout_ = VK_NULL_HANDLE; }
|
||||
if (descPool) { vkDestroyDescriptorPool(device, descPool, nullptr); descPool = VK_NULL_HANDLE; }
|
||||
if (samplerSetLayout) { vkDestroyDescriptorSetLayout(device, samplerSetLayout, nullptr); samplerSetLayout = VK_NULL_HANDLE; }
|
||||
if (quadVB) { vmaDestroyBuffer(alloc, quadVB, quadVBAlloc); quadVB = VK_NULL_HANDLE; }
|
||||
|
||||
for (auto& tex : zoneTextures) {
|
||||
if (tex) tex->destroy(device, alloc);
|
||||
}
|
||||
zoneTextures.clear();
|
||||
zoneTextureSlots_.clear();
|
||||
|
||||
if (fogTexture_) { fogTexture_->destroy(device, alloc); fogTexture_.reset(); }
|
||||
if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); }
|
||||
|
||||
initialized = false;
|
||||
vkCtx = nullptr;
|
||||
}
|
||||
|
||||
void CompositeRenderer::destroyZoneTextures(std::vector<Zone>& /*zones*/) {
|
||||
if (!vkCtx) return;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
|
||||
for (auto& tex : zoneTextures) {
|
||||
if (tex) tex->destroy(device, alloc);
|
||||
}
|
||||
zoneTextures.clear();
|
||||
|
||||
for (auto& slots : zoneTextureSlots_) {
|
||||
for (auto& tex : slots.tileTextures) tex = nullptr;
|
||||
slots.tilesLoaded = false;
|
||||
for (auto& ov : slots.overlays) {
|
||||
for (auto& t : ov.tiles) t = nullptr;
|
||||
ov.tilesLoaded = false;
|
||||
}
|
||||
}
|
||||
zoneTextureSlots_.clear();
|
||||
}
|
||||
|
||||
void CompositeRenderer::loadZoneTextures(int zoneIdx, std::vector<Zone>& zones,
|
||||
const std::string& mapName) {
|
||||
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return;
|
||||
ensureTextureSlots(zones.size(), zones);
|
||||
auto& slots = zoneTextureSlots_[zoneIdx];
|
||||
if (slots.tilesLoaded) return;
|
||||
slots.tilesLoaded = true;
|
||||
|
||||
const auto& zone = zones[zoneIdx];
|
||||
const std::string& folder = zone.areaName;
|
||||
if (folder.empty()) return;
|
||||
|
||||
LOG_INFO("loadZoneTextures: zone[", zoneIdx, "] areaName='", zone.areaName,
|
||||
"' areaID=", zone.areaID, " mapName='", mapName, "'");
|
||||
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
int loaded = 0;
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
std::string path = "Interface\\WorldMap\\" + folder + "\\" +
|
||||
folder + std::to_string(i + 1) + ".blp";
|
||||
auto blpImage = assetManager->loadTexture(path);
|
||||
if (!blpImage.isValid()) {
|
||||
slots.tileTextures[i] = nullptr;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto tex = std::make_unique<VkTexture>();
|
||||
tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height,
|
||||
VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||
tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
||||
|
||||
slots.tileTextures[i] = tex.get();
|
||||
zoneTextures.push_back(std::move(tex));
|
||||
loaded++;
|
||||
}
|
||||
|
||||
LOG_INFO("CompositeRenderer: loaded ", loaded, "/12 tiles for '", folder, "'");
|
||||
}
|
||||
|
||||
void CompositeRenderer::loadOverlayTextures(int zoneIdx, std::vector<Zone>& zones) {
|
||||
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return;
|
||||
ensureTextureSlots(zones.size(), zones);
|
||||
|
||||
const auto& zone = zones[zoneIdx];
|
||||
auto& slots = zoneTextureSlots_[zoneIdx];
|
||||
if (zone.overlays.empty()) return;
|
||||
|
||||
const std::string& folder = zone.areaName;
|
||||
if (folder.empty()) return;
|
||||
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
int totalLoaded = 0;
|
||||
|
||||
for (size_t oi = 0; oi < zone.overlays.size(); oi++) {
|
||||
const auto& ov = zone.overlays[oi];
|
||||
auto& ovSlots = slots.overlays[oi];
|
||||
if (ovSlots.tilesLoaded) continue;
|
||||
ovSlots.tilesLoaded = true;
|
||||
|
||||
int tileCount = ov.tileCols * ov.tileRows;
|
||||
for (int t = 0; t < tileCount; t++) {
|
||||
std::string tileName = ov.textureName + std::to_string(t + 1);
|
||||
std::string path = "Interface\\WorldMap\\" + folder + "\\" + tileName + ".blp";
|
||||
auto blpImage = assetManager->loadTexture(path);
|
||||
if (!blpImage.isValid()) {
|
||||
ovSlots.tiles[t] = nullptr;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto tex = std::make_unique<VkTexture>();
|
||||
tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height,
|
||||
VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||
tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
||||
|
||||
ovSlots.tiles[t] = tex.get();
|
||||
zoneTextures.push_back(std::move(tex));
|
||||
totalLoaded++;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("CompositeRenderer: loaded ", totalLoaded, " overlay tiles for '", folder, "'");
|
||||
}
|
||||
|
||||
void CompositeRenderer::detachZoneTextures() {
|
||||
if (!zoneTextures.empty() && vkCtx) {
|
||||
// Defer destruction until all in-flight frames have completed.
|
||||
// This avoids calling vkDeviceWaitIdle mid-frame, which can trigger
|
||||
// driver TDR (GPU device lost) under heavy rendering load.
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
VmaAllocator alloc = vkCtx->getAllocator();
|
||||
auto captured = std::make_shared<std::vector<std::unique_ptr<VkTexture>>>(
|
||||
std::move(zoneTextures));
|
||||
vkCtx->deferAfterAllFrameFences([device, alloc, captured]() {
|
||||
for (auto& tex : *captured) {
|
||||
if (tex) tex->destroy(device, alloc);
|
||||
}
|
||||
});
|
||||
}
|
||||
zoneTextures.clear();
|
||||
|
||||
// Clear CPU-side tracking immediately so new zones get fresh loads
|
||||
for (auto& slots : zoneTextureSlots_) {
|
||||
for (auto& tex : slots.tileTextures) tex = nullptr;
|
||||
slots.tilesLoaded = false;
|
||||
for (auto& ov : slots.overlays) {
|
||||
for (auto& t : ov.tiles) t = nullptr;
|
||||
ov.tilesLoaded = false;
|
||||
}
|
||||
}
|
||||
zoneTextureSlots_.clear();
|
||||
}
|
||||
|
||||
void CompositeRenderer::flushStaleTextures() {
|
||||
// No-op: texture cleanup is now handled by deferAfterAllFrameFences
|
||||
// in detachZoneTextures. Kept for API compatibility.
|
||||
}
|
||||
|
||||
void CompositeRenderer::requestComposite(int zoneIdx) {
|
||||
pendingCompositeIdx_ = zoneIdx;
|
||||
}
|
||||
|
||||
bool CompositeRenderer::hasAnyTile(int zoneIdx) const {
|
||||
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zoneTextureSlots_.size()))
|
||||
return false;
|
||||
const auto& slots = zoneTextureSlots_[zoneIdx];
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (slots.tileTextures[i] != nullptr) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CompositeRenderer::compositePass(VkCommandBuffer cmd,
|
||||
const std::vector<Zone>& zones,
|
||||
const std::unordered_set<int>& exploredOverlays,
|
||||
bool hasServerMask) {
|
||||
if (!initialized || pendingCompositeIdx_ < 0 || !compositeTarget) return;
|
||||
if (pendingCompositeIdx_ >= static_cast<int>(zones.size())) {
|
||||
pendingCompositeIdx_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
int zoneIdx = pendingCompositeIdx_;
|
||||
pendingCompositeIdx_ = -1;
|
||||
|
||||
if (compositedIdx_ == zoneIdx) return;
|
||||
ensureTextureSlots(zones.size(), zones);
|
||||
|
||||
const auto& zone = zones[zoneIdx];
|
||||
const auto& slots = zoneTextureSlots_[zoneIdx];
|
||||
uint32_t frameIdx = vkCtx->getCurrentFrame();
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
|
||||
// Update tile descriptor sets for this frame
|
||||
for (int i = 0; i < 12; i++) {
|
||||
VkTexture* tileTex = slots.tileTextures[i];
|
||||
if (!tileTex || !tileTex->isValid()) continue;
|
||||
|
||||
VkDescriptorImageInfo imgInfo = tileTex->descriptorInfo();
|
||||
VkWriteDescriptorSet write{};
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.dstSet = tileDescSets[frameIdx][i];
|
||||
write.dstBinding = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &imgInfo;
|
||||
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
||||
}
|
||||
|
||||
// Begin off-screen render pass
|
||||
VkClearColorValue clearColor = {{ 0.05f, 0.08f, 0.12f, 1.0f }};
|
||||
compositeTarget->beginPass(cmd, clearColor);
|
||||
|
||||
VkDeviceSize offset = 0;
|
||||
vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset);
|
||||
|
||||
// --- Pass 1: Draw base map tiles (opaque) ---
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipeline);
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (!slots.tileTextures[i] || !slots.tileTextures[i]->isValid()) continue;
|
||||
|
||||
int col = i % GRID_COLS;
|
||||
int row = i / GRID_COLS;
|
||||
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
tilePipelineLayout, 0, 1,
|
||||
&tileDescSets[frameIdx][i], 0, nullptr);
|
||||
|
||||
WorldMapTilePush push{};
|
||||
push.gridOffset = glm::vec2(static_cast<float>(col), static_cast<float>(row));
|
||||
push.gridCols = static_cast<float>(GRID_COLS);
|
||||
push.gridRows = static_cast<float>(GRID_ROWS);
|
||||
vkCmdPushConstants(cmd, tilePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, sizeof(push), &push);
|
||||
|
||||
vkCmdDraw(cmd, 6, 1, 0, 0);
|
||||
}
|
||||
|
||||
// --- Draw explored overlay textures on top of the base map ---
|
||||
bool hasOverlays = !zone.overlays.empty() && zone.areaID != 0;
|
||||
if (hasOverlays && overlayPipeline_) {
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline_);
|
||||
|
||||
uint32_t descSlot = 0;
|
||||
for (int oi = 0; oi < static_cast<int>(zone.overlays.size()); oi++) {
|
||||
if (exploredOverlays.count(oi) == 0) continue;
|
||||
const auto& ov = zone.overlays[oi];
|
||||
const auto& ovSlots = slots.overlays[oi];
|
||||
|
||||
for (int t = 0; t < static_cast<int>(ovSlots.tiles.size()); t++) {
|
||||
if (!ovSlots.tiles[t] || !ovSlots.tiles[t]->isValid()) continue;
|
||||
if (descSlot >= MAX_OVERLAY_TILES) break;
|
||||
|
||||
VkDescriptorImageInfo ovImgInfo = ovSlots.tiles[t]->descriptorInfo();
|
||||
VkWriteDescriptorSet ovWrite{};
|
||||
ovWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
ovWrite.dstSet = overlayDescSets_[frameIdx][descSlot];
|
||||
ovWrite.dstBinding = 0;
|
||||
ovWrite.descriptorCount = 1;
|
||||
ovWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
ovWrite.pImageInfo = &ovImgInfo;
|
||||
vkUpdateDescriptorSets(device, 1, &ovWrite, 0, nullptr);
|
||||
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
overlayPipelineLayout_, 0, 1,
|
||||
&overlayDescSets_[frameIdx][descSlot], 0, nullptr);
|
||||
|
||||
int tileCol = t % ov.tileCols;
|
||||
int tileRow = t / ov.tileCols;
|
||||
|
||||
float px = static_cast<float>(ov.offsetX + tileCol * TILE_PX);
|
||||
float py = static_cast<float>(ov.offsetY + tileRow * TILE_PX);
|
||||
|
||||
OverlayPush ovPush{};
|
||||
ovPush.gridOffset = glm::vec2(px / static_cast<float>(TILE_PX),
|
||||
py / static_cast<float>(TILE_PX));
|
||||
ovPush.gridCols = static_cast<float>(GRID_COLS);
|
||||
ovPush.gridRows = static_cast<float>(GRID_ROWS);
|
||||
ovPush.tintColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, sizeof(WorldMapTilePush), &ovPush);
|
||||
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
16, sizeof(glm::vec4), &ovPush.tintColor);
|
||||
vkCmdDraw(cmd, 6, 1, 0, 0);
|
||||
|
||||
descSlot++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Draw fog of war overlay over unexplored areas ---
|
||||
if (hasServerMask && zone.areaID != 0 && overlayPipeline_ && fogDescSet_) {
|
||||
bool hasAnyExplored = false;
|
||||
for (int oi = 0; oi < static_cast<int>(zone.overlays.size()); oi++) {
|
||||
if (exploredOverlays.count(oi) > 0) { hasAnyExplored = true; break; }
|
||||
}
|
||||
if (!hasAnyExplored && !zone.overlays.empty()) {
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, overlayPipeline_);
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
overlayPipelineLayout_, 0, 1,
|
||||
&fogDescSet_, 0, nullptr);
|
||||
|
||||
OverlayPush fogPush{};
|
||||
fogPush.gridOffset = glm::vec2(0.0f, 0.0f);
|
||||
fogPush.gridCols = 1.0f;
|
||||
fogPush.gridRows = 1.0f;
|
||||
fogPush.tintColor = glm::vec4(0.15f, 0.15f, 0.2f, 0.55f);
|
||||
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, sizeof(WorldMapTilePush), &fogPush);
|
||||
vkCmdPushConstants(cmd, overlayPipelineLayout_, VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
16, sizeof(glm::vec4), &fogPush.tintColor);
|
||||
vkCmdDraw(cmd, 6, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
compositeTarget->endPass(cmd);
|
||||
compositedIdx_ = zoneIdx;
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
228
src/rendering/world_map/coordinate_projection.cpp
Normal file
228
src/rendering/world_map/coordinate_projection.cpp
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
// coordinate_projection.cpp — Pure coordinate math for world map UV projection.
|
||||
// Extracted from WorldMap::renderPosToMapUV, findBestContinentForPlayer,
|
||||
// findZoneForPlayer, zoneBelongsToContinent, getContinentProjectionBounds,
|
||||
// isRootContinent, isLeafContinent (Phase 2 of refactoring plan).
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
// ── Continent classification helpers ─────────────────────────
|
||||
|
||||
bool isRootContinent(const std::vector<Zone>& zones, int idx) {
|
||||
if (idx < 0 || idx >= static_cast<int>(zones.size())) return false;
|
||||
const auto& c = zones[idx];
|
||||
if (c.areaID != 0 || c.wmaID == 0) return false;
|
||||
for (const auto& z : zones) {
|
||||
if (z.areaID == 0 && z.parentWorldMapID == c.wmaID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isLeafContinent(const std::vector<Zone>& zones, int idx) {
|
||||
if (idx < 0 || idx >= static_cast<int>(zones.size())) return false;
|
||||
const auto& c = zones[idx];
|
||||
if (c.areaID != 0) return false;
|
||||
return c.parentWorldMapID != 0;
|
||||
}
|
||||
|
||||
// ── UV projection ────────────────────────────────────────────
|
||||
|
||||
glm::vec2 renderPosToMapUV(const glm::vec3& renderPos,
|
||||
const ZoneBounds& bounds,
|
||||
bool isContinent) {
|
||||
float wowX = renderPos.y;
|
||||
float wowY = renderPos.x;
|
||||
|
||||
float denom_h = bounds.locLeft - bounds.locRight;
|
||||
float denom_v = bounds.locTop - bounds.locBottom;
|
||||
if (std::abs(denom_h) < 0.001f || std::abs(denom_v) < 0.001f)
|
||||
return glm::vec2(0.5f, 0.5f);
|
||||
|
||||
float u = (bounds.locLeft - wowX) / denom_h;
|
||||
float v = (bounds.locTop - wowY) / denom_v;
|
||||
|
||||
if (isContinent) {
|
||||
constexpr float kVScale = 1.0f;
|
||||
constexpr float kVOffset = -0.15f;
|
||||
v = (v - 0.5f) * kVScale + 0.5f + kVOffset;
|
||||
}
|
||||
return glm::vec2(u, v);
|
||||
}
|
||||
|
||||
// ── Continent projection bounds ──────────────────────────────
|
||||
|
||||
bool getContinentProjectionBounds(const std::vector<Zone>& zones,
|
||||
int contIdx,
|
||||
float& left, float& right,
|
||||
float& top, float& bottom) {
|
||||
if (contIdx < 0 || contIdx >= static_cast<int>(zones.size())) return false;
|
||||
const auto& cont = zones[contIdx];
|
||||
if (cont.areaID != 0) return false;
|
||||
|
||||
if (std::abs(cont.bounds.locLeft - cont.bounds.locRight) > 0.001f &&
|
||||
std::abs(cont.bounds.locTop - cont.bounds.locBottom) > 0.001f) {
|
||||
left = cont.bounds.locLeft; right = cont.bounds.locRight;
|
||||
top = cont.bounds.locTop; bottom = cont.bounds.locBottom;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<float> northEdges, southEdges, westEdges, eastEdges;
|
||||
for (int zi = 0; zi < static_cast<int>(zones.size()); zi++) {
|
||||
if (!zoneBelongsToContinent(zones, zi, contIdx)) continue;
|
||||
const auto& z = zones[zi];
|
||||
if (std::abs(z.bounds.locLeft - z.bounds.locRight) < 0.001f ||
|
||||
std::abs(z.bounds.locTop - z.bounds.locBottom) < 0.001f) continue;
|
||||
northEdges.push_back(std::max(z.bounds.locLeft, z.bounds.locRight));
|
||||
southEdges.push_back(std::min(z.bounds.locLeft, z.bounds.locRight));
|
||||
westEdges.push_back(std::max(z.bounds.locTop, z.bounds.locBottom));
|
||||
eastEdges.push_back(std::min(z.bounds.locTop, z.bounds.locBottom));
|
||||
}
|
||||
|
||||
if (northEdges.size() < 3) {
|
||||
left = cont.bounds.locLeft; right = cont.bounds.locRight;
|
||||
top = cont.bounds.locTop; bottom = cont.bounds.locBottom;
|
||||
return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f;
|
||||
}
|
||||
|
||||
left = *std::max_element(northEdges.begin(), northEdges.end());
|
||||
right = *std::min_element(southEdges.begin(), southEdges.end());
|
||||
top = *std::max_element(westEdges.begin(), westEdges.end());
|
||||
bottom = *std::min_element(eastEdges.begin(), eastEdges.end());
|
||||
|
||||
if (left <= right || top <= bottom) {
|
||||
left = cont.bounds.locLeft; right = cont.bounds.locRight;
|
||||
top = cont.bounds.locTop; bottom = cont.bounds.locBottom;
|
||||
}
|
||||
return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f;
|
||||
}
|
||||
|
||||
// ── Player position lookups ──────────────────────────────────
|
||||
|
||||
int findBestContinentForPlayer(const std::vector<Zone>& zones,
|
||||
const glm::vec3& playerRenderPos) {
|
||||
float wowX = playerRenderPos.y;
|
||||
float wowY = playerRenderPos.x;
|
||||
|
||||
int bestIdx = -1;
|
||||
float bestArea = std::numeric_limits<float>::max();
|
||||
float bestCenterDist2 = std::numeric_limits<float>::max();
|
||||
|
||||
bool hasLeaf = false;
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
if (zones[i].areaID == 0 && !isRootContinent(zones, i)) {
|
||||
hasLeaf = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& z = zones[i];
|
||||
if (z.areaID != 0) continue;
|
||||
if (hasLeaf && isRootContinent(zones, i)) continue;
|
||||
|
||||
float minX = std::min(z.bounds.locLeft, z.bounds.locRight);
|
||||
float maxX = std::max(z.bounds.locLeft, z.bounds.locRight);
|
||||
float minY = std::min(z.bounds.locTop, z.bounds.locBottom);
|
||||
float maxY = std::max(z.bounds.locTop, z.bounds.locBottom);
|
||||
float spanX = maxX - minX;
|
||||
float spanY = maxY - minY;
|
||||
if (spanX < 0.001f || spanY < 0.001f) continue;
|
||||
|
||||
bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY);
|
||||
float area = spanX * spanY;
|
||||
if (contains) {
|
||||
if (area < bestArea) { bestArea = area; bestIdx = i; }
|
||||
} else if (bestIdx < 0) {
|
||||
float cx = (minX + maxX) * 0.5f, cy = (minY + maxY) * 0.5f;
|
||||
float dist2 = (wowX - cx) * (wowX - cx) + (wowY - cy) * (wowY - cy);
|
||||
if (dist2 < bestCenterDist2) { bestCenterDist2 = dist2; bestIdx = i; }
|
||||
}
|
||||
}
|
||||
return bestIdx;
|
||||
}
|
||||
|
||||
int findZoneForPlayer(const std::vector<Zone>& zones,
|
||||
const glm::vec3& playerRenderPos) {
|
||||
float wowX = playerRenderPos.y;
|
||||
float wowY = playerRenderPos.x;
|
||||
|
||||
int bestIdx = -1;
|
||||
float bestArea = std::numeric_limits<float>::max();
|
||||
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& z = zones[i];
|
||||
if (z.areaID == 0) continue;
|
||||
|
||||
float minX = std::min(z.bounds.locLeft, z.bounds.locRight);
|
||||
float maxX = std::max(z.bounds.locLeft, z.bounds.locRight);
|
||||
float minY = std::min(z.bounds.locTop, z.bounds.locBottom);
|
||||
float maxY = std::max(z.bounds.locTop, z.bounds.locBottom);
|
||||
float spanX = maxX - minX, spanY = maxY - minY;
|
||||
if (spanX < 0.001f || spanY < 0.001f) continue;
|
||||
|
||||
if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) {
|
||||
float area = spanX * spanY;
|
||||
if (area < bestArea) { bestArea = area; bestIdx = i; }
|
||||
}
|
||||
}
|
||||
return bestIdx;
|
||||
}
|
||||
|
||||
// ── Zone–continent relationship ──────────────────────────────
|
||||
|
||||
bool zoneBelongsToContinent(const std::vector<Zone>& zones,
|
||||
int zoneIdx, int contIdx) {
|
||||
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return false;
|
||||
if (contIdx < 0 || contIdx >= static_cast<int>(zones.size())) return false;
|
||||
|
||||
const auto& z = zones[zoneIdx];
|
||||
const auto& cont = zones[contIdx];
|
||||
if (z.areaID == 0) return false;
|
||||
|
||||
// Prefer explicit parent link if available
|
||||
if (z.parentWorldMapID != 0 && cont.wmaID != 0)
|
||||
return z.parentWorldMapID == cont.wmaID;
|
||||
|
||||
// Fallback: spatial overlap heuristic
|
||||
auto rectMinX = [](const Zone& a) { return std::min(a.bounds.locLeft, a.bounds.locRight); };
|
||||
auto rectMaxX = [](const Zone& a) { return std::max(a.bounds.locLeft, a.bounds.locRight); };
|
||||
auto rectMinY = [](const Zone& a) { return std::min(a.bounds.locTop, a.bounds.locBottom); };
|
||||
auto rectMaxY = [](const Zone& a) { return std::max(a.bounds.locTop, a.bounds.locBottom); };
|
||||
|
||||
float zMinX = rectMinX(z), zMaxX = rectMaxX(z);
|
||||
float zMinY = rectMinY(z), zMaxY = rectMaxY(z);
|
||||
if ((zMaxX - zMinX) < 0.001f || (zMaxY - zMinY) < 0.001f) return false;
|
||||
|
||||
int bestContIdx = -1;
|
||||
float bestOverlap = 0.0f;
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& c = zones[i];
|
||||
if (c.areaID != 0) continue;
|
||||
float cMinX = rectMinX(c), cMaxX = rectMaxX(c);
|
||||
float cMinY = rectMinY(c), cMaxY = rectMaxY(c);
|
||||
if ((cMaxX - cMinX) < 0.001f || (cMaxY - cMinY) < 0.001f) continue;
|
||||
|
||||
float ox = std::max(0.0f, std::min(zMaxX, cMaxX) - std::max(zMinX, cMinX));
|
||||
float oy = std::max(0.0f, std::min(zMaxY, cMaxY) - std::max(zMinY, cMinY));
|
||||
float overlap = ox * oy;
|
||||
if (overlap > bestOverlap) { bestOverlap = overlap; bestContIdx = i; }
|
||||
}
|
||||
if (bestContIdx >= 0) return bestContIdx == contIdx;
|
||||
|
||||
float centerX = (z.bounds.locLeft + z.bounds.locRight) * 0.5f;
|
||||
float centerY = (z.bounds.locTop + z.bounds.locBottom) * 0.5f;
|
||||
return centerX >= rectMinX(cont) && centerX <= rectMaxX(cont) &&
|
||||
centerY >= rectMinY(cont) && centerY <= rectMaxY(cont);
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
485
src/rendering/world_map/data_repository.cpp
Normal file
485
src/rendering/world_map/data_repository.cpp
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
// data_repository.cpp — DBC data loading, ZMP pixel map, and zone/POI/overlay storage.
|
||||
// Extracted from WorldMap::loadZonesFromDBC, loadPOIData, buildCosmicView
|
||||
// (Phase 5 of refactoring plan).
|
||||
#include "rendering/world_map/data_repository.hpp"
|
||||
#include "rendering/world_map/map_resolver.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
#include "game/game_utils.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void DataRepository::clear() {
|
||||
zones_.clear();
|
||||
poiMarkers_.clear();
|
||||
cosmicMaps_.clear();
|
||||
azerothRegions_.clear();
|
||||
exploreFlagByAreaId_.clear();
|
||||
areaNameByAreaId_.clear();
|
||||
areaIdToZoneIdx_.clear();
|
||||
zmpZoneBounds_.clear();
|
||||
zmpGrid_.fill(0);
|
||||
zmpLoaded_ = false;
|
||||
cosmicIdx_ = -1;
|
||||
worldIdx_ = -1;
|
||||
currentMapId_ = -1;
|
||||
cosmicEnabled_ = true;
|
||||
poisLoaded_ = false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// DBC zone loading (moved from WorldMap::loadZonesFromDBC)
|
||||
// --------------------------------------------------------
|
||||
|
||||
void DataRepository::loadZones(const std::string& mapName,
|
||||
pipeline::AssetManager& assetManager) {
|
||||
if (!zones_.empty()) return;
|
||||
|
||||
const auto* activeLayout = pipeline::getActiveDBCLayout();
|
||||
const auto* mapL = activeLayout ? activeLayout->getLayout("Map") : nullptr;
|
||||
|
||||
int mapID = -1;
|
||||
auto mapDbc = assetManager.loadDBC("Map.dbc");
|
||||
if (mapDbc && mapDbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
|
||||
std::string dir = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
|
||||
if (dir == mapName) {
|
||||
mapID = static_cast<int>(mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0));
|
||||
LOG_INFO("DataRepository: Map.dbc '", mapName, "' -> mapID=", mapID);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapID < 0) {
|
||||
mapID = folderToMapId(mapName);
|
||||
if (mapID < 0) {
|
||||
LOG_WARNING("DataRepository: unknown map '", mapName, "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Use expansion-aware DBC layout when available; fall back to WotLK stock field
|
||||
// indices (ID=0, ParentAreaNum=2, ExploreFlag=3) when layout metadata is missing.
|
||||
const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr;
|
||||
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> childBitsByParent;
|
||||
auto areaDbc = assetManager.loadDBC("AreaTable.dbc");
|
||||
// Bug fix: old code used > 3 which covers core fields (ID=0, ParentAreaNum=2,
|
||||
// ExploreFlag=3). The > 11 threshold broke exploration for DBC variants with
|
||||
// 4-11 fields. Load core exploration data with > 3; area name only when > 11.
|
||||
if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) {
|
||||
const uint32_t fieldCount = areaDbc->getFieldCount();
|
||||
const uint32_t parentField = atL ? (*atL)["ParentAreaNum"] : 2;
|
||||
for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) {
|
||||
const uint32_t areaId = areaDbc->getUInt32(i, atL ? (*atL)["ID"] : 0);
|
||||
const uint32_t exploreFlag = areaDbc->getUInt32(i, atL ? (*atL)["ExploreFlag"] : 3);
|
||||
const uint32_t parentArea = areaDbc->getUInt32(i, parentField);
|
||||
if (areaId != 0) exploreFlagByAreaId[areaId] = exploreFlag;
|
||||
if (parentArea != 0) childBitsByParent[parentArea].push_back(exploreFlag);
|
||||
// Cache area display name (field 11 = AreaName_lang enUS)
|
||||
if (areaId != 0 && fieldCount > 11) {
|
||||
std::string areaDispName = areaDbc->getString(i, 11);
|
||||
if (!areaDispName.empty())
|
||||
areaNameByAreaId_[areaId] = std::move(areaDispName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto wmaDbc = assetManager.loadDBC("WorldMapArea.dbc");
|
||||
if (!wmaDbc || !wmaDbc->isLoaded()) {
|
||||
LOG_WARNING("DataRepository: WorldMapArea.dbc not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr;
|
||||
|
||||
int continentIdx = -1;
|
||||
for (uint32_t i = 0; i < wmaDbc->getRecordCount(); i++) {
|
||||
uint32_t recMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["MapID"] : 1);
|
||||
if (static_cast<int>(recMapID) != mapID) continue;
|
||||
|
||||
Zone zone;
|
||||
zone.wmaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ID"] : 0);
|
||||
zone.areaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["AreaID"] : 2);
|
||||
zone.areaName = wmaDbc->getString(i, wmaL ? (*wmaL)["AreaName"] : 3);
|
||||
zone.bounds.locLeft = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocLeft"] : 4);
|
||||
zone.bounds.locRight = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocRight"] : 5);
|
||||
zone.bounds.locTop = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocTop"] : 6);
|
||||
zone.bounds.locBottom = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocBottom"] : 7);
|
||||
zone.displayMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["DisplayMapID"] : 8);
|
||||
zone.parentWorldMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ParentWorldMapID"] : 10);
|
||||
// Collect the zone's own AreaBit plus all subzone AreaBits
|
||||
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
|
||||
if (exploreIt != exploreFlagByAreaId.end())
|
||||
zone.exploreBits.push_back(exploreIt->second);
|
||||
auto childIt = childBitsByParent.find(zone.areaID);
|
||||
if (childIt != childBitsByParent.end()) {
|
||||
for (uint32_t bit : childIt->second)
|
||||
zone.exploreBits.push_back(bit);
|
||||
}
|
||||
|
||||
int idx = static_cast<int>(zones_.size());
|
||||
|
||||
LOG_INFO("DataRepository: zone[", idx, "] areaID=", zone.areaID,
|
||||
" '", zone.areaName, "' L=", zone.bounds.locLeft,
|
||||
" R=", zone.bounds.locRight, " T=", zone.bounds.locTop,
|
||||
" B=", zone.bounds.locBottom);
|
||||
|
||||
if (zone.areaID == 0 && continentIdx < 0)
|
||||
continentIdx = idx;
|
||||
|
||||
zones_.push_back(std::move(zone));
|
||||
}
|
||||
|
||||
// Derive continent bounds from child zones if missing
|
||||
for (int ci = 0; ci < static_cast<int>(zones_.size()); ci++) {
|
||||
auto& cont = zones_[ci];
|
||||
if (cont.areaID != 0) continue;
|
||||
if (std::abs(cont.bounds.locLeft) > 0.001f || std::abs(cont.bounds.locRight) > 0.001f ||
|
||||
std::abs(cont.bounds.locTop) > 0.001f || std::abs(cont.bounds.locBottom) > 0.001f)
|
||||
continue;
|
||||
|
||||
bool first = true;
|
||||
for (const auto& z : zones_) {
|
||||
if (z.areaID == 0) continue;
|
||||
if (std::abs(z.bounds.locLeft - z.bounds.locRight) < 0.001f ||
|
||||
std::abs(z.bounds.locTop - z.bounds.locBottom) < 0.001f)
|
||||
continue;
|
||||
if (z.parentWorldMapID != 0 && cont.wmaID != 0 && z.parentWorldMapID != cont.wmaID)
|
||||
continue;
|
||||
|
||||
if (first) {
|
||||
cont.bounds.locLeft = z.bounds.locLeft; cont.bounds.locRight = z.bounds.locRight;
|
||||
cont.bounds.locTop = z.bounds.locTop; cont.bounds.locBottom = z.bounds.locBottom;
|
||||
first = false;
|
||||
} else {
|
||||
cont.bounds.locLeft = std::min(cont.bounds.locLeft, z.bounds.locLeft);
|
||||
cont.bounds.locRight = std::max(cont.bounds.locRight, z.bounds.locRight);
|
||||
cont.bounds.locTop = std::min(cont.bounds.locTop, z.bounds.locTop);
|
||||
cont.bounds.locBottom = std::max(cont.bounds.locBottom, z.bounds.locBottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentMapId_ = mapID;
|
||||
exploreFlagByAreaId_ = exploreFlagByAreaId; // cache for overlay exploration checks
|
||||
LOG_INFO("DataRepository: loaded ", zones_.size(), " zones for mapID=", mapID,
|
||||
", continentIdx=", continentIdx);
|
||||
|
||||
// Build wmaID → zone index lookup
|
||||
std::unordered_map<uint32_t, int> wmaIdToZoneIdx;
|
||||
for (int i = 0; i < static_cast<int>(zones_.size()); i++)
|
||||
wmaIdToZoneIdx[zones_[i].wmaID] = i;
|
||||
|
||||
// Parse WorldMapOverlay.dbc → attach overlay entries to their zones
|
||||
auto wmoDbc = assetManager.loadDBC("WorldMapOverlay.dbc");
|
||||
if (wmoDbc && wmoDbc->isLoaded()) {
|
||||
// WotLK field layout:
|
||||
// 0:ID, 1:WorldMapAreaID, 2-5:AreaTableID[4],
|
||||
// 6:MapPointX, 7:MapPointY, 8:TextureName(str),
|
||||
// 9:TextureWidth, 10:TextureHeight,
|
||||
// 11:OffsetX, 12:OffsetY, 13-16:HitRect
|
||||
int totalOverlays = 0;
|
||||
for (uint32_t i = 0; i < wmoDbc->getRecordCount(); i++) {
|
||||
uint32_t wmaID = wmoDbc->getUInt32(i, 1);
|
||||
auto it = wmaIdToZoneIdx.find(wmaID);
|
||||
if (it == wmaIdToZoneIdx.end()) continue;
|
||||
|
||||
OverlayEntry ov;
|
||||
ov.areaIDs[0] = wmoDbc->getUInt32(i, 2);
|
||||
ov.areaIDs[1] = wmoDbc->getUInt32(i, 3);
|
||||
ov.areaIDs[2] = wmoDbc->getUInt32(i, 4);
|
||||
ov.areaIDs[3] = wmoDbc->getUInt32(i, 5);
|
||||
ov.textureName = wmoDbc->getString(i, 8);
|
||||
ov.texWidth = static_cast<uint16_t>(wmoDbc->getUInt32(i, 9));
|
||||
ov.texHeight = static_cast<uint16_t>(wmoDbc->getUInt32(i, 10));
|
||||
ov.offsetX = static_cast<uint16_t>(wmoDbc->getUInt32(i, 11));
|
||||
ov.offsetY = static_cast<uint16_t>(wmoDbc->getUInt32(i, 12));
|
||||
// HitRect (fields 13-16): fast AABB pre-filter for subzone hover
|
||||
ov.hitRectLeft = static_cast<uint16_t>(wmoDbc->getUInt32(i, 13));
|
||||
ov.hitRectRight = static_cast<uint16_t>(wmoDbc->getUInt32(i, 14));
|
||||
ov.hitRectTop = static_cast<uint16_t>(wmoDbc->getUInt32(i, 15));
|
||||
ov.hitRectBottom = static_cast<uint16_t>(wmoDbc->getUInt32(i, 16));
|
||||
|
||||
if (ov.textureName.empty() || ov.texWidth == 0 || ov.texHeight == 0) continue;
|
||||
|
||||
ov.tileCols = (ov.texWidth + 255) / 256;
|
||||
ov.tileRows = (ov.texHeight + 255) / 256;
|
||||
|
||||
zones_[it->second].overlays.push_back(std::move(ov));
|
||||
totalOverlays++;
|
||||
}
|
||||
LOG_INFO("DataRepository: loaded ", totalOverlays, " overlay entries from WorldMapOverlay.dbc");
|
||||
}
|
||||
|
||||
// Create a synthetic "Cosmic" zone for the cross-world map (Azeroth + Outland)
|
||||
{
|
||||
Zone cosmic;
|
||||
cosmic.areaName = "Cosmic";
|
||||
cosmicIdx_ = static_cast<int>(zones_.size());
|
||||
zones_.push_back(std::move(cosmic));
|
||||
LOG_INFO("DataRepository: added synthetic Cosmic zone at index ", cosmicIdx_);
|
||||
}
|
||||
|
||||
// Create a synthetic "World" zone for the combined world overview map
|
||||
{
|
||||
Zone world;
|
||||
world.areaName = "World";
|
||||
worldIdx_ = static_cast<int>(zones_.size());
|
||||
zones_.push_back(std::move(world));
|
||||
LOG_INFO("DataRepository: added synthetic World zone at index ", worldIdx_);
|
||||
}
|
||||
|
||||
// Load area POI data (towns, dungeons, etc.)
|
||||
loadPOIs(assetManager);
|
||||
|
||||
// Build areaID → zone index lookup for ZMP resolution
|
||||
for (int i = 0; i < static_cast<int>(zones_.size()); i++) {
|
||||
if (zones_[i].areaID != 0)
|
||||
areaIdToZoneIdx_[zones_[i].areaID] = i;
|
||||
}
|
||||
|
||||
// Load ZMP pixel map for continent-level hover detection
|
||||
loadZmpPixelMap(mapName, assetManager);
|
||||
|
||||
// Build views based on active expansion
|
||||
buildCosmicView();
|
||||
buildAzerothView();
|
||||
}
|
||||
|
||||
int DataRepository::getExpansionLevel() {
|
||||
if (game::isClassicLikeExpansion()) return 0;
|
||||
if (game::isActiveExpansion("tbc")) return 1;
|
||||
return 2; // WotLK and above
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// ZMP pixel map loading
|
||||
// --------------------------------------------------------
|
||||
|
||||
void DataRepository::loadZmpPixelMap(const std::string& continentName,
|
||||
pipeline::AssetManager& assetManager) {
|
||||
zmpGrid_.fill(0);
|
||||
zmpLoaded_ = false;
|
||||
|
||||
// ZMP path: Interface\WorldMap\{name_lower}.zmp
|
||||
std::string lower = continentName;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
std::string zmpPath = "Interface\\WorldMap\\" + lower + ".zmp";
|
||||
|
||||
auto data = assetManager.readFileOptional(zmpPath);
|
||||
if (data.empty()) {
|
||||
LOG_INFO("DataRepository: ZMP not found at '", zmpPath, "' (ok for non-continent maps)");
|
||||
return;
|
||||
}
|
||||
|
||||
// ZMP is a 128x128 grid of uint32 = 65536 bytes
|
||||
constexpr size_t kExpectedSize = ZMP_SIZE * ZMP_SIZE * sizeof(uint32_t);
|
||||
if (data.size() != kExpectedSize) {
|
||||
LOG_WARNING("DataRepository: ZMP '", zmpPath, "' unexpected size ",
|
||||
data.size(), " (expected ", kExpectedSize, ")");
|
||||
return;
|
||||
}
|
||||
|
||||
std::memcpy(zmpGrid_.data(), data.data(), kExpectedSize);
|
||||
zmpLoaded_ = true;
|
||||
|
||||
// Count non-zero cells and find grid extent for diagnostic
|
||||
int nonZero = 0;
|
||||
int maxRow = -1, maxCol = -1;
|
||||
for (int r = 0; r < ZMP_SIZE; r++) {
|
||||
for (int c = 0; c < ZMP_SIZE; c++) {
|
||||
if (zmpGrid_[r * ZMP_SIZE + c] != 0) {
|
||||
nonZero++;
|
||||
if (r > maxRow) maxRow = r;
|
||||
if (c > maxCol) maxCol = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_INFO("DataRepository: loaded ZMP '", zmpPath, "' — ",
|
||||
nonZero, "/", ZMP_SIZE * ZMP_SIZE, " non-zero cells, "
|
||||
"maxCol=", maxCol, " maxRow=", maxRow,
|
||||
" (if ~125/~111 → maps to 1024x768 FBO, if ~127/~127 → maps to 1002x668 visible)");
|
||||
|
||||
// Derive zone bounding boxes from ZMP grid
|
||||
buildZmpZoneBounds();
|
||||
}
|
||||
|
||||
void DataRepository::buildZmpZoneBounds() {
|
||||
zmpZoneBounds_.clear();
|
||||
if (!zmpLoaded_) return;
|
||||
|
||||
// Scan the 128x128 ZMP grid and find the bounding box of each area ID's pixels.
|
||||
// The ZMP grid maps directly to the visible 1002×668 content area, so
|
||||
// (col/128, row/128) gives display UV without FBO conversion.
|
||||
struct RawRect { int minCol = ZMP_SIZE, maxCol = -1, minRow = ZMP_SIZE, maxRow = -1; };
|
||||
std::unordered_map<uint32_t, RawRect> areaRects;
|
||||
|
||||
for (int row = 0; row < ZMP_SIZE; row++) {
|
||||
for (int col = 0; col < ZMP_SIZE; col++) {
|
||||
uint32_t areaId = zmpGrid_[row * ZMP_SIZE + col];
|
||||
if (areaId == 0) continue;
|
||||
auto& r = areaRects[areaId];
|
||||
r.minCol = std::min(r.minCol, col);
|
||||
r.maxCol = std::max(r.maxCol, col);
|
||||
r.minRow = std::min(r.minRow, row);
|
||||
r.maxRow = std::max(r.maxRow, row);
|
||||
}
|
||||
}
|
||||
|
||||
// Map area ID bounding boxes → zone index bounding boxes.
|
||||
// Multiple area IDs may resolve to the same zone, so union their rects.
|
||||
constexpr float kInvSize = 1.0f / static_cast<float>(ZMP_SIZE);
|
||||
int mapped = 0;
|
||||
for (const auto& [areaId, rect] : areaRects) {
|
||||
int zi = zoneIndexForAreaId(areaId);
|
||||
if (zi < 0) continue;
|
||||
|
||||
float uMin = static_cast<float>(rect.minCol) * kInvSize;
|
||||
float uMax = static_cast<float>(rect.maxCol + 1) * kInvSize;
|
||||
float vMin = static_cast<float>(rect.minRow) * kInvSize;
|
||||
float vMax = static_cast<float>(rect.maxRow + 1) * kInvSize;
|
||||
|
||||
auto it = zmpZoneBounds_.find(zi);
|
||||
if (it != zmpZoneBounds_.end()) {
|
||||
// Union with existing rect for this zone
|
||||
it->second.uMin = std::min(it->second.uMin, uMin);
|
||||
it->second.uMax = std::max(it->second.uMax, uMax);
|
||||
it->second.vMin = std::min(it->second.vMin, vMin);
|
||||
it->second.vMax = std::max(it->second.vMax, vMax);
|
||||
} else {
|
||||
ZmpRect zr;
|
||||
zr.uMin = uMin; zr.uMax = uMax;
|
||||
zr.vMin = vMin; zr.vMax = vMax;
|
||||
zr.valid = true;
|
||||
zmpZoneBounds_[zi] = zr;
|
||||
mapped++;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("DataRepository: built ZMP zone bounds for ", mapped, " zones from ",
|
||||
areaRects.size(), " area IDs");
|
||||
}
|
||||
|
||||
int DataRepository::zoneIndexForAreaId(uint32_t areaId) const {
|
||||
if (areaId == 0) return -1;
|
||||
auto it = areaIdToZoneIdx_.find(areaId);
|
||||
if (it != areaIdToZoneIdx_.end()) return it->second;
|
||||
|
||||
// Fallback: check if areaId is a sub-zone whose parent is in our zone list.
|
||||
// Some ZMP cells reference sub-area IDs not directly in WorldMapArea.dbc.
|
||||
// Walk the AreaTable parent chain via exploreFlagByAreaId_ (which was built
|
||||
// from AreaTable.dbc and includes parentArea relationships).
|
||||
// For now, iterate zones looking for one whose overlays reference this areaId.
|
||||
for (int i = 0; i < static_cast<int>(zones_.size()); i++) {
|
||||
for (const auto& ov : zones_[i].overlays) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
if (ov.areaIDs[j] == areaId) return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void DataRepository::loadPOIs(pipeline::AssetManager& assetManager) {
|
||||
if (poisLoaded_) return;
|
||||
poisLoaded_ = true;
|
||||
|
||||
auto poiDbc = assetManager.loadDBC("AreaPOI.dbc");
|
||||
if (!poiDbc || !poiDbc->isLoaded()) {
|
||||
LOG_INFO("DataRepository: AreaPOI.dbc not found, skipping POI markers");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t fieldCount = poiDbc->getFieldCount();
|
||||
if (fieldCount < 17) {
|
||||
LOG_WARNING("DataRepository: AreaPOI.dbc has too few fields (", fieldCount, ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// AreaPOI.dbc field layout (WotLK 3.3.5a):
|
||||
// 0:ID, 1:Importance, 2-10:Icon[9], 11:FactionID,
|
||||
// 12:X, 13:Y, 14:Z, 15:MapID,
|
||||
// 16:Name_lang (enUS), ...
|
||||
int loaded = 0;
|
||||
for (uint32_t i = 0; i < poiDbc->getRecordCount(); i++) {
|
||||
POI poi;
|
||||
poi.id = poiDbc->getUInt32(i, 0);
|
||||
poi.importance = poiDbc->getUInt32(i, 1);
|
||||
poi.iconType = poiDbc->getUInt32(i, 2);
|
||||
poi.factionId = poiDbc->getUInt32(i, 11);
|
||||
poi.wowX = poiDbc->getFloat(i, 12);
|
||||
poi.wowY = poiDbc->getFloat(i, 13);
|
||||
poi.wowZ = poiDbc->getFloat(i, 14);
|
||||
poi.mapId = poiDbc->getUInt32(i, 15);
|
||||
poi.name = poiDbc->getString(i, 16);
|
||||
|
||||
if (poi.name.empty()) continue;
|
||||
|
||||
poiMarkers_.push_back(std::move(poi));
|
||||
loaded++;
|
||||
}
|
||||
|
||||
// Sort by importance ascending so high-importance POIs are drawn last (on top)
|
||||
std::sort(poiMarkers_.begin(), poiMarkers_.end(),
|
||||
[](const POI& a, const POI& b) { return a.importance < b.importance; });
|
||||
|
||||
LOG_INFO("DataRepository: loaded ", loaded, " POI markers from AreaPOI.dbc");
|
||||
}
|
||||
|
||||
void DataRepository::buildCosmicView(int /*expLevel*/) {
|
||||
cosmicMaps_.clear();
|
||||
|
||||
if (game::isClassicLikeExpansion()) {
|
||||
// Vanilla/Classic: No cosmic view — skip from WORLD straight to CONTINENT.
|
||||
cosmicEnabled_ = false;
|
||||
LOG_INFO("DataRepository: Classic mode — cosmic view disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
cosmicEnabled_ = true;
|
||||
|
||||
// Azeroth (EK + Kalimdor) — always present; bottom-right region of cosmic map
|
||||
cosmicMaps_.push_back({0, "Azeroth", 0.58f, 0.05f, 0.95f, 0.95f});
|
||||
|
||||
if (game::isActiveExpansion("tbc") || game::isActiveExpansion("wotlk")) {
|
||||
// TBC+: Add Outland — top-left region of cosmic map
|
||||
cosmicMaps_.push_back({530, "Outland", 0.05f, 0.10f, 0.55f, 0.90f});
|
||||
}
|
||||
|
||||
LOG_INFO("DataRepository: cosmic view built with ", cosmicMaps_.size(), " landmasses");
|
||||
}
|
||||
|
||||
void DataRepository::buildAzerothView(int /*expLevel*/) {
|
||||
azerothRegions_.clear();
|
||||
|
||||
// Clickable continent regions on the Azeroth world map (azeroth1-12.blp).
|
||||
// UV coordinates are approximate positions of each landmass on the combined map.
|
||||
|
||||
// Eastern Kingdoms — right side of the Azeroth map
|
||||
azerothRegions_.push_back({0, mapDisplayName(0), 0.55f, 0.05f, 0.95f, 0.95f});
|
||||
|
||||
// Kalimdor — left side of the Azeroth map
|
||||
azerothRegions_.push_back({1, mapDisplayName(1), 0.05f, 0.10f, 0.45f, 0.95f});
|
||||
|
||||
if (game::isActiveExpansion("wotlk")) {
|
||||
// WotLK: Northrend — top-center of the Azeroth map
|
||||
azerothRegions_.push_back({571, mapDisplayName(571), 0.30f, 0.0f, 0.72f, 0.28f});
|
||||
}
|
||||
|
||||
LOG_INFO("DataRepository: Azeroth view built with ", azerothRegions_.size(), " continent regions");
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
119
src/rendering/world_map/exploration_state.cpp
Normal file
119
src/rendering/world_map/exploration_state.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// exploration_state.cpp — Fog of war / exploration tracking implementation.
|
||||
// Extracted from WorldMap::updateExploration, setServerExplorationMask
|
||||
// (Phase 3 of refactoring plan).
|
||||
#include "rendering/world_map/exploration_state.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void ExplorationState::setServerMask(const std::vector<uint32_t>& masks, bool hasData) {
|
||||
if (!hasData || masks.empty()) {
|
||||
// New session or no data yet — reset both server mask and local accumulation
|
||||
if (hasServerMask_) {
|
||||
locallyExploredZones_.clear();
|
||||
}
|
||||
hasServerMask_ = false;
|
||||
serverMask_.clear();
|
||||
return;
|
||||
}
|
||||
hasServerMask_ = true;
|
||||
serverMask_ = masks;
|
||||
}
|
||||
|
||||
bool ExplorationState::isBitSet(uint32_t bitIndex) const {
|
||||
if (!hasServerMask_ || serverMask_.empty()) return false;
|
||||
const size_t word = bitIndex / 32;
|
||||
if (word >= serverMask_.size()) return false;
|
||||
return (serverMask_[word] & (1u << (bitIndex % 32))) != 0;
|
||||
}
|
||||
|
||||
void ExplorationState::update(const std::vector<Zone>& zones,
|
||||
const glm::vec3& playerRenderPos,
|
||||
int currentZoneIdx,
|
||||
const std::unordered_map<uint32_t, uint32_t>& exploreFlagByAreaId) {
|
||||
overlaysChanged_ = false;
|
||||
|
||||
if (hasServerMask_) {
|
||||
exploredZones_.clear();
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& z = zones[i];
|
||||
if (z.areaID == 0 || z.exploreBits.empty()) continue;
|
||||
for (uint32_t bit : z.exploreBits) {
|
||||
if (isBitSet(bit)) {
|
||||
exploredZones_.insert(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also reveal the zone the player is currently standing in so the map isn't
|
||||
// pitch-black the moment they first enter a new zone.
|
||||
int curZone = findZoneForPlayer(zones, playerRenderPos);
|
||||
if (curZone >= 0) exploredZones_.insert(curZone);
|
||||
|
||||
// Per-overlay exploration: check each overlay's areaIDs against the exploration mask
|
||||
std::unordered_set<int> newExploredOverlays;
|
||||
if (currentZoneIdx >= 0 && currentZoneIdx < static_cast<int>(zones.size())) {
|
||||
const auto& curZoneData = zones[currentZoneIdx];
|
||||
for (int oi = 0; oi < static_cast<int>(curZoneData.overlays.size()); oi++) {
|
||||
const auto& ov = curZoneData.overlays[oi];
|
||||
bool revealed = false;
|
||||
for (int a = 0; a < 4; a++) {
|
||||
if (ov.areaIDs[a] == 0) continue;
|
||||
auto flagIt = exploreFlagByAreaId.find(ov.areaIDs[a]);
|
||||
if (flagIt != exploreFlagByAreaId.end() && isBitSet(flagIt->second)) {
|
||||
revealed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (revealed) newExploredOverlays.insert(oi);
|
||||
}
|
||||
}
|
||||
if (newExploredOverlays != exploredOverlays_) {
|
||||
exploredOverlays_ = std::move(newExploredOverlays);
|
||||
overlaysChanged_ = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Server mask unavailable — fall back to locally-accumulated position tracking.
|
||||
float wowX = playerRenderPos.y;
|
||||
float wowY = playerRenderPos.x;
|
||||
|
||||
bool foundPos = false;
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
const auto& z = zones[i];
|
||||
if (z.areaID == 0) continue;
|
||||
float minX = std::min(z.bounds.locLeft, z.bounds.locRight);
|
||||
float maxX = std::max(z.bounds.locLeft, z.bounds.locRight);
|
||||
float minY = std::min(z.bounds.locTop, z.bounds.locBottom);
|
||||
float maxY = std::max(z.bounds.locTop, z.bounds.locBottom);
|
||||
if (maxX - minX < 0.001f || maxY - minY < 0.001f) continue;
|
||||
if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) {
|
||||
locallyExploredZones_.insert(i);
|
||||
foundPos = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPos) {
|
||||
int zoneIdx = findZoneForPlayer(zones, playerRenderPos);
|
||||
if (zoneIdx >= 0) locallyExploredZones_.insert(zoneIdx);
|
||||
}
|
||||
|
||||
// Display the accumulated local set
|
||||
exploredZones_ = locallyExploredZones_;
|
||||
|
||||
// Without server mask, mark all overlays as explored (no fog of war)
|
||||
exploredOverlays_.clear();
|
||||
if (currentZoneIdx >= 0 && currentZoneIdx < static_cast<int>(zones.size())) {
|
||||
for (int oi = 0; oi < static_cast<int>(zones[currentZoneIdx].overlays.size()); oi++)
|
||||
exploredOverlays_.insert(oi);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
57
src/rendering/world_map/input_handler.cpp
Normal file
57
src/rendering/world_map/input_handler.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// input_handler.cpp — Input processing for the world map.
|
||||
// Extracted from WorldMap::render (Phase 9 of refactoring plan).
|
||||
#include "rendering/world_map/input_handler.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
InputResult InputHandler::process(ViewLevel currentLevel,
|
||||
int hoveredZoneIdx,
|
||||
bool cosmicEnabled) {
|
||||
InputResult result;
|
||||
auto& input = core::Input::getInstance();
|
||||
|
||||
// ESC closes the map
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
|
||||
result.action = InputAction::CLOSE;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scroll wheel zoom
|
||||
auto& io = ImGui::GetIO();
|
||||
float wheelDelta = io.MouseWheel;
|
||||
if (std::abs(wheelDelta) < 0.001f)
|
||||
wheelDelta = input.getMouseWheelDelta();
|
||||
|
||||
if (wheelDelta > 0.0f) {
|
||||
result.action = InputAction::ZOOM_IN;
|
||||
return result;
|
||||
} else if (wheelDelta < 0.0f) {
|
||||
result.action = InputAction::ZOOM_OUT;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Continent view: left-click on hovered zone (from previous frame)
|
||||
if (currentLevel == ViewLevel::CONTINENT && hoveredZoneIdx >= 0 &&
|
||||
input.isMouseButtonJustPressed(1)) {
|
||||
result.action = InputAction::CLICK_ZONE;
|
||||
result.targetIdx = hoveredZoneIdx;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Right-click to go back (zone → continent; continent → world)
|
||||
if (io.MouseClicked[1]) {
|
||||
result.action = InputAction::RIGHT_CLICK_BACK;
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
53
src/rendering/world_map/layers/coordinate_display.cpp
Normal file
53
src/rendering/world_map/layers/coordinate_display.cpp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// coordinate_display.cpp — WoW coordinates under cursor on the world map.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/coordinate_display.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void CoordinateDisplay::render(const LayerContext& ctx) {
|
||||
if (ctx.currentZoneIdx < 0) return;
|
||||
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
auto& io = ImGui::GetIO();
|
||||
ImVec2 mp = io.MousePos;
|
||||
if (mp.x < ctx.imgMin.x || mp.x > ctx.imgMin.x + ctx.displayW ||
|
||||
mp.y < ctx.imgMin.y || mp.y > ctx.imgMin.y + ctx.displayH)
|
||||
return;
|
||||
|
||||
float mu = (mp.x - ctx.imgMin.x) / ctx.displayW;
|
||||
float mv = (mp.y - ctx.imgMin.y) / ctx.displayH;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
float left = zone.bounds.locLeft, right = zone.bounds.locRight;
|
||||
float top = zone.bounds.locTop, bottom = zone.bounds.locBottom;
|
||||
if (zone.areaID == 0) {
|
||||
float l, r, t, b;
|
||||
getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b);
|
||||
left = l; right = r; top = t; bottom = b;
|
||||
// Undo the kVOffset applied during renderPosToMapUV for continent
|
||||
constexpr float kVOffset = -0.15f;
|
||||
mv -= kVOffset;
|
||||
}
|
||||
|
||||
float hWowX = left - mu * (left - right);
|
||||
float hWowY = top - mv * (top - bottom);
|
||||
|
||||
char coordBuf[32];
|
||||
snprintf(coordBuf, sizeof(coordBuf), "%.0f, %.0f", hWowX, hWowY);
|
||||
ImVec2 coordSz = ImGui::CalcTextSize(coordBuf);
|
||||
float cx = ctx.imgMin.x + ctx.displayW - coordSz.x - 8.0f;
|
||||
float cy = ctx.imgMin.y + ctx.displayH - coordSz.y - 8.0f;
|
||||
ctx.drawList->AddText(ImVec2(cx + 1.0f, cy + 1.0f), IM_COL32(0, 0, 0, 180), coordBuf);
|
||||
ctx.drawList->AddText(ImVec2(cx, cy), IM_COL32(220, 210, 150, 230), coordBuf);
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
54
src/rendering/world_map/layers/corpse_marker_layer.cpp
Normal file
54
src/rendering/world_map/layers/corpse_marker_layer.cpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// corpse_marker_layer.cpp — Death corpse X marker on the world map.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/corpse_marker_layer.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void CorpseMarkerLayer::render(const LayerContext& ctx) {
|
||||
if (!hasCorpse_) return;
|
||||
if (ctx.currentZoneIdx < 0) return;
|
||||
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
ZoneBounds bounds = zone.bounds;
|
||||
bool isContinent = zone.areaID == 0;
|
||||
if (isContinent) {
|
||||
float l, r, t, b;
|
||||
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||
bounds = {l, r, t, b};
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec2 uv = renderPosToMapUV(corpseRenderPos_, bounds, isContinent);
|
||||
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) return;
|
||||
|
||||
float cx = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||
float cy = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||
constexpr float R = 5.0f;
|
||||
constexpr float T = 1.8f;
|
||||
// Dark outline
|
||||
ctx.drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R),
|
||||
IM_COL32(0, 0, 0, 220), T + 1.5f);
|
||||
ctx.drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R),
|
||||
IM_COL32(0, 0, 0, 220), T + 1.5f);
|
||||
// Bone-white X
|
||||
ctx.drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R),
|
||||
IM_COL32(230, 220, 200, 240), T);
|
||||
ctx.drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R),
|
||||
IM_COL32(230, 220, 200, 240), T);
|
||||
// Tooltip on hover
|
||||
ImVec2 mp = ImGui::GetMousePos();
|
||||
float dx = mp.x - cx, dy = mp.y - cy;
|
||||
if (dx * dx + dy * dy < 64.0f) {
|
||||
ImGui::SetTooltip("Your corpse");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
52
src/rendering/world_map/layers/party_dot_layer.cpp
Normal file
52
src/rendering/world_map/layers/party_dot_layer.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// party_dot_layer.cpp — Party member position dots on the world map.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/party_dot_layer.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void PartyDotLayer::render(const LayerContext& ctx) {
|
||||
if (!dots_ || dots_->empty()) return;
|
||||
if (ctx.currentZoneIdx < 0) return;
|
||||
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
ZoneBounds bounds = zone.bounds;
|
||||
bool isContinent = zone.areaID == 0;
|
||||
if (isContinent) {
|
||||
float l, r, t, b;
|
||||
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||
bounds = {l, r, t, b};
|
||||
}
|
||||
}
|
||||
|
||||
ImFont* font = ImGui::GetFont();
|
||||
for (const auto& dot : *dots_) {
|
||||
glm::vec2 uv = renderPosToMapUV(dot.renderPos, bounds, isContinent);
|
||||
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||
ctx.drawList->AddCircleFilled(ImVec2(px, py), 5.0f, dot.color);
|
||||
ctx.drawList->AddCircle(ImVec2(px, py), 5.0f, IM_COL32(0, 0, 0, 200), 0, 1.5f);
|
||||
if (!dot.name.empty()) {
|
||||
ImVec2 mp = ImGui::GetMousePos();
|
||||
float dx = mp.x - px, dy = mp.y - py;
|
||||
if (dx * dx + dy * dy <= 49.0f) {
|
||||
ImGui::SetTooltip("%s", dot.name.c_str());
|
||||
}
|
||||
ImVec2 nameSz = font->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f, dot.name.c_str());
|
||||
float tx = px - nameSz.x * 0.5f;
|
||||
float ty = py - nameSz.y - 7.0f;
|
||||
ctx.drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), dot.name.c_str());
|
||||
ctx.drawList->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 220), dot.name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
57
src/rendering/world_map/layers/player_marker_layer.cpp
Normal file
57
src/rendering/world_map/layers/player_marker_layer.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// player_marker_layer.cpp — Directional player arrow on the world map.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/player_marker_layer.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void PlayerMarkerLayer::render(const LayerContext& ctx) {
|
||||
if (ctx.currentZoneIdx < 0) return;
|
||||
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
ZoneBounds bounds = zone.bounds;
|
||||
bool isContinent = zone.areaID == 0;
|
||||
|
||||
// In continent view, only show the player marker if they are actually
|
||||
// in a zone belonging to this continent (don't bleed across continents).
|
||||
if (isContinent) {
|
||||
int playerZone = findZoneForPlayer(*ctx.zones, ctx.playerRenderPos);
|
||||
if (playerZone < 0 || !zoneBelongsToContinent(*ctx.zones, playerZone, ctx.currentZoneIdx))
|
||||
return;
|
||||
float l, r, t, b;
|
||||
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||
bounds = {l, r, t, b};
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec2 playerUV = renderPosToMapUV(ctx.playerRenderPos, bounds, isContinent);
|
||||
if (playerUV.x < 0.0f || playerUV.x > 1.0f ||
|
||||
playerUV.y < 0.0f || playerUV.y > 1.0f) return;
|
||||
|
||||
float px = ctx.imgMin.x + playerUV.x * ctx.displayW;
|
||||
float py = ctx.imgMin.y + playerUV.y * ctx.displayH;
|
||||
|
||||
// Directional arrow: render-space (cos,sin) maps to screen (-dx,-dy)
|
||||
float yawRad = glm::radians(ctx.playerYawDeg);
|
||||
float adx = -std::cos(yawRad);
|
||||
float ady = -std::sin(yawRad);
|
||||
float apx = -ady, apy = adx;
|
||||
constexpr float TIP = 9.0f;
|
||||
constexpr float TAIL = 4.0f;
|
||||
constexpr float HALF = 5.0f;
|
||||
ImVec2 tip(px + adx * TIP, py + ady * TIP);
|
||||
ImVec2 bl (px - adx * TAIL + apx * HALF, py - ady * TAIL + apy * HALF);
|
||||
ImVec2 br (px - adx * TAIL - apx * HALF, py - ady * TAIL - apy * HALF);
|
||||
ctx.drawList->AddTriangleFilled(tip, bl, br, IM_COL32(255, 40, 40, 255));
|
||||
ctx.drawList->AddTriangle(tip, bl, br, IM_COL32(0, 0, 0, 200), 1.5f);
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
102
src/rendering/world_map/layers/poi_marker_layer.cpp
Normal file
102
src/rendering/world_map/layers/poi_marker_layer.cpp
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// poi_marker_layer.cpp — Town/dungeon/capital POI icons on the world map.
|
||||
// Extracted from WorldMap::renderPOIMarkers (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/poi_marker_layer.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void POIMarkerLayer::render(const LayerContext& ctx) {
|
||||
if (!markers_ || markers_->empty()) return;
|
||||
if (ctx.currentZoneIdx < 0) return;
|
||||
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
ZoneBounds bounds = zone.bounds;
|
||||
bool isContinent = zone.areaID == 0;
|
||||
if (isContinent) {
|
||||
float l, r, t, b;
|
||||
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||
bounds = {l, r, t, b};
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 mp = ImGui::GetMousePos();
|
||||
ImFont* font = ImGui::GetFont();
|
||||
|
||||
for (const auto& poi : *markers_) {
|
||||
if (static_cast<int>(poi.mapId) != ctx.currentMapId) continue;
|
||||
|
||||
glm::vec3 rPos = core::coords::canonicalToRender(
|
||||
glm::vec3(poi.wowX, poi.wowY, poi.wowZ));
|
||||
glm::vec2 uv = renderPosToMapUV(rPos, bounds, isContinent);
|
||||
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||
|
||||
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||
|
||||
float iconSize = (poi.importance >= 2) ? 7.0f :
|
||||
(poi.importance >= 1) ? 5.0f : 3.0f;
|
||||
|
||||
ImU32 fillColor, borderColor;
|
||||
if (poi.factionId == 469) {
|
||||
fillColor = IM_COL32(60, 120, 255, 200);
|
||||
borderColor = IM_COL32(20, 60, 180, 220);
|
||||
} else if (poi.factionId == 67) {
|
||||
fillColor = IM_COL32(255, 60, 60, 200);
|
||||
borderColor = IM_COL32(180, 20, 20, 220);
|
||||
} else {
|
||||
fillColor = IM_COL32(255, 215, 0, 200);
|
||||
borderColor = IM_COL32(180, 150, 0, 220);
|
||||
}
|
||||
|
||||
if (poi.importance >= 2) {
|
||||
ctx.drawList->AddCircleFilled(ImVec2(px, py), iconSize + 2.0f,
|
||||
IM_COL32(255, 255, 200, 30));
|
||||
ctx.drawList->AddCircleFilled(ImVec2(px, py), iconSize, fillColor);
|
||||
ctx.drawList->AddCircle(ImVec2(px, py), iconSize, borderColor, 0, 2.0f);
|
||||
} else if (poi.importance >= 1) {
|
||||
float H = iconSize;
|
||||
ImVec2 top2(px, py - H);
|
||||
ImVec2 right2(px + H, py );
|
||||
ImVec2 bot2(px, py + H);
|
||||
ImVec2 left2(px - H, py );
|
||||
ctx.drawList->AddQuadFilled(top2, right2, bot2, left2, fillColor);
|
||||
ctx.drawList->AddQuad(top2, right2, bot2, left2, borderColor, 1.2f);
|
||||
} else {
|
||||
ctx.drawList->AddCircleFilled(ImVec2(px, py), iconSize, fillColor);
|
||||
ctx.drawList->AddCircle(ImVec2(px, py), iconSize, borderColor, 0, 1.0f);
|
||||
}
|
||||
|
||||
if (poi.importance >= 1 && ctx.viewLevel == ViewLevel::ZONE && !poi.name.empty()) {
|
||||
float fontSize = (poi.importance >= 2) ? ImGui::GetFontSize() * 0.85f :
|
||||
ImGui::GetFontSize() * 0.75f;
|
||||
ImVec2 nameSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, poi.name.c_str());
|
||||
float tx = px - nameSz.x * 0.5f;
|
||||
float ty = py + iconSize + 2.0f;
|
||||
ctx.drawList->AddText(font, fontSize,
|
||||
ImVec2(tx + 1.0f, ty + 1.0f),
|
||||
IM_COL32(0, 0, 0, 180), poi.name.c_str());
|
||||
ctx.drawList->AddText(font, fontSize,
|
||||
ImVec2(tx, ty), IM_COL32(255, 255, 255, 210),
|
||||
poi.name.c_str());
|
||||
}
|
||||
|
||||
float dx = mp.x - px, dy = mp.y - py;
|
||||
float hitRadius = iconSize + 4.0f;
|
||||
if (dx * dx + dy * dy < hitRadius * hitRadius && !poi.name.empty()) {
|
||||
if (!poi.description.empty())
|
||||
ImGui::SetTooltip("%s\n%s", poi.name.c_str(), poi.description.c_str());
|
||||
else
|
||||
ImGui::SetTooltip("%s", poi.name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
60
src/rendering/world_map/layers/quest_poi_layer.cpp
Normal file
60
src/rendering/world_map/layers/quest_poi_layer.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// quest_poi_layer.cpp — Quest objective markers on the world map.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/quest_poi_layer.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void QuestPOILayer::render(const LayerContext& ctx) {
|
||||
if (!pois_ || pois_->empty()) return;
|
||||
if (ctx.currentZoneIdx < 0) return;
|
||||
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
ZoneBounds bounds = zone.bounds;
|
||||
bool isContinent = zone.areaID == 0;
|
||||
if (isContinent) {
|
||||
float l, r, t, b;
|
||||
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||
bounds = {l, r, t, b};
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 mp = ImGui::GetMousePos();
|
||||
ImFont* qFont = ImGui::GetFont();
|
||||
for (const auto& qp : *pois_) {
|
||||
glm::vec3 rPos = core::coords::canonicalToRender(
|
||||
glm::vec3(qp.wowX, qp.wowY, 0.0f));
|
||||
glm::vec2 uv = renderPosToMapUV(rPos, bounds, isContinent);
|
||||
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||
|
||||
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||
|
||||
ctx.drawList->AddCircleFilled(ImVec2(px, py), 5.0f, IM_COL32(0, 210, 255, 220));
|
||||
ctx.drawList->AddCircle(ImVec2(px, py), 5.0f, IM_COL32(255, 215, 0, 220), 0, 1.5f);
|
||||
|
||||
if (!qp.name.empty()) {
|
||||
ImVec2 nameSz = qFont->CalcTextSizeA(ImGui::GetFontSize() * 0.85f, FLT_MAX, 0.0f, qp.name.c_str());
|
||||
float tx = px - nameSz.x * 0.5f;
|
||||
float ty = py - nameSz.y - 7.0f;
|
||||
ctx.drawList->AddText(qFont, ImGui::GetFontSize() * 0.85f,
|
||||
ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), qp.name.c_str());
|
||||
ctx.drawList->AddText(qFont, ImGui::GetFontSize() * 0.85f,
|
||||
ImVec2(tx, ty), IM_COL32(255, 230, 100, 230), qp.name.c_str());
|
||||
}
|
||||
float mdx = mp.x - px, mdy = mp.y - py;
|
||||
if (mdx * mdx + mdy * mdy < 49.0f && !qp.name.empty()) {
|
||||
ImGui::SetTooltip("%s\n(Quest Objective)", qp.name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
100
src/rendering/world_map/layers/subzone_tooltip_layer.cpp
Normal file
100
src/rendering/world_map/layers/subzone_tooltip_layer.cpp
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// subzone_tooltip_layer.cpp — Overlay area hover labels in zone view.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/subzone_tooltip_layer.hpp"
|
||||
#include <imgui.h>
|
||||
#include <limits>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void SubzoneTooltipLayer::render(const LayerContext& ctx) {
|
||||
if (ctx.viewLevel != ViewLevel::ZONE) return;
|
||||
if (ctx.currentZoneIdx < 0 || !ctx.zones) return;
|
||||
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
if (mp.x < ctx.imgMin.x || mp.x > ctx.imgMin.x + ctx.displayW ||
|
||||
mp.y < ctx.imgMin.y || mp.y > ctx.imgMin.y + ctx.displayH)
|
||||
return;
|
||||
|
||||
float mu = (mp.x - ctx.imgMin.x) / ctx.displayW;
|
||||
float mv = (mp.y - ctx.imgMin.y) / ctx.displayH;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
std::string hoveredName;
|
||||
bool hoveredExplored = false;
|
||||
float bestArea = std::numeric_limits<float>::max();
|
||||
|
||||
float fboW = static_cast<float>(ctx.fboW);
|
||||
float fboH = static_cast<float>(ctx.fboH);
|
||||
|
||||
// Mouse position in FBO pixel coordinates (used for HitRect AABB test)
|
||||
float pixelX = mu * fboW;
|
||||
float pixelY = mv * fboH;
|
||||
|
||||
for (int oi = 0; oi < static_cast<int>(zone.overlays.size()); oi++) {
|
||||
const auto& ov = zone.overlays[oi];
|
||||
|
||||
// ── Hybrid Approach: Zone view uses HitRect AABB pre-filter ──
|
||||
// WorldMapOverlay.dbc fields 13-16 define a hit-test rectangle.
|
||||
// Only overlays whose HitRect contains the mouse need further testing.
|
||||
// This is Blizzard's optimization to avoid sampling every overlay.
|
||||
bool hasHitRect = (ov.hitRectRight > ov.hitRectLeft &&
|
||||
ov.hitRectBottom > ov.hitRectTop);
|
||||
if (hasHitRect) {
|
||||
if (pixelX < static_cast<float>(ov.hitRectLeft) ||
|
||||
pixelX > static_cast<float>(ov.hitRectRight) ||
|
||||
pixelY < static_cast<float>(ov.hitRectTop) ||
|
||||
pixelY > static_cast<float>(ov.hitRectBottom)) {
|
||||
continue; // Mouse outside HitRect — skip this overlay
|
||||
}
|
||||
} else {
|
||||
// Fallback: use overlay offset+size AABB (old behaviour)
|
||||
float ovLeft = static_cast<float>(ov.offsetX) / fboW;
|
||||
float ovTop = static_cast<float>(ov.offsetY) / fboH;
|
||||
float ovRight = static_cast<float>(ov.offsetX + ov.texWidth) / fboW;
|
||||
float ovBottom = static_cast<float>(ov.offsetY + ov.texHeight) / fboH;
|
||||
|
||||
if (mu < ovLeft || mu > ovRight || mv < ovTop || mv > ovBottom)
|
||||
continue;
|
||||
}
|
||||
|
||||
float area = static_cast<float>(ov.texWidth) * static_cast<float>(ov.texHeight);
|
||||
if (area < bestArea) {
|
||||
bestArea = area;
|
||||
// Find display name from the first valid area ID
|
||||
for (int a = 0; a < 4; a++) {
|
||||
if (ov.areaIDs[a] == 0) continue;
|
||||
if (ctx.areaNameByAreaId) {
|
||||
auto nameIt = ctx.areaNameByAreaId->find(ov.areaIDs[a]);
|
||||
if (nameIt != ctx.areaNameByAreaId->end()) {
|
||||
hoveredName = nameIt->second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hoveredExplored = ctx.exploredOverlays &&
|
||||
ctx.exploredOverlays->count(oi) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hoveredName.empty()) {
|
||||
std::string label = hoveredName;
|
||||
if (!hoveredExplored)
|
||||
label += " (Unexplored)";
|
||||
|
||||
ImVec2 labelSz = ImGui::CalcTextSize(label.c_str());
|
||||
float lx = ctx.imgMin.x + (ctx.displayW - labelSz.x) * 0.5f;
|
||||
float ly = ctx.imgMin.y + 6.0f;
|
||||
ImU32 labelCol = hoveredExplored
|
||||
? IM_COL32(255, 230, 150, 240)
|
||||
: IM_COL32(160, 160, 160, 200);
|
||||
ctx.drawList->AddText(ImVec2(lx + 1.0f, ly + 1.0f),
|
||||
IM_COL32(0, 0, 0, 200), label.c_str());
|
||||
ctx.drawList->AddText(ImVec2(lx, ly), labelCol, label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
62
src/rendering/world_map/layers/taxi_node_layer.cpp
Normal file
62
src/rendering/world_map/layers/taxi_node_layer.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// taxi_node_layer.cpp — Flight master diamond icons on the world map.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/taxi_node_layer.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void TaxiNodeLayer::render(const LayerContext& ctx) {
|
||||
if (!nodes_ || nodes_->empty()) return;
|
||||
if (ctx.currentZoneIdx < 0) return;
|
||||
if (ctx.viewLevel != ViewLevel::ZONE && ctx.viewLevel != ViewLevel::CONTINENT) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
const auto& zone = (*ctx.zones)[ctx.currentZoneIdx];
|
||||
ZoneBounds bounds = zone.bounds;
|
||||
bool isContinent = zone.areaID == 0;
|
||||
if (isContinent) {
|
||||
float l, r, t, b;
|
||||
if (getContinentProjectionBounds(*ctx.zones, ctx.currentZoneIdx, l, r, t, b)) {
|
||||
bounds = {l, r, t, b};
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 mp = ImGui::GetMousePos();
|
||||
for (const auto& node : *nodes_) {
|
||||
if (!node.known) continue;
|
||||
if (static_cast<int>(node.mapId) != ctx.currentMapId) continue;
|
||||
|
||||
glm::vec3 rPos = core::coords::canonicalToRender(
|
||||
glm::vec3(node.wowX, node.wowY, node.wowZ));
|
||||
glm::vec2 uv = renderPosToMapUV(rPos, bounds, isContinent);
|
||||
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||
|
||||
float px = ctx.imgMin.x + uv.x * ctx.displayW;
|
||||
float py = ctx.imgMin.y + uv.y * ctx.displayH;
|
||||
|
||||
constexpr float H = 5.0f;
|
||||
ImVec2 top2(px, py - H);
|
||||
ImVec2 right2(px + H, py );
|
||||
ImVec2 bot2(px, py + H);
|
||||
ImVec2 left2(px - H, py );
|
||||
ctx.drawList->AddQuadFilled(top2, right2, bot2, left2,
|
||||
IM_COL32(255, 215, 0, 230));
|
||||
ctx.drawList->AddQuad(top2, right2, bot2, left2,
|
||||
IM_COL32(80, 50, 0, 200), 1.2f);
|
||||
|
||||
if (!node.name.empty()) {
|
||||
float mdx = mp.x - px, mdy = mp.y - py;
|
||||
if (mdx * mdx + mdy * mdy < 49.0f) {
|
||||
ImGui::SetTooltip("%s\n(Flight Master)", node.name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
294
src/rendering/world_map/layers/zone_highlight_layer.cpp
Normal file
294
src/rendering/world_map/layers/zone_highlight_layer.cpp
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
// zone_highlight_layer.cpp — Continent view zone rectangles + hover effects.
|
||||
// Extracted from WorldMap::renderZoneHighlights (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/layers/zone_highlight_layer.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <backends/imgui_impl_vulkan.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
ZoneHighlightLayer::~ZoneHighlightLayer() {
|
||||
// At shutdown vkDeviceWaitIdle has been called, so immediate cleanup is safe.
|
||||
if (vkCtx_) {
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
VmaAllocator alloc = vkCtx_->getAllocator();
|
||||
for (auto& [name, entry] : highlights_) {
|
||||
if (entry.imguiDS) ImGui_ImplVulkan_RemoveTexture(entry.imguiDS);
|
||||
if (entry.texture) entry.texture->destroy(device, alloc);
|
||||
}
|
||||
}
|
||||
highlights_.clear();
|
||||
missingHighlights_.clear();
|
||||
}
|
||||
|
||||
void ZoneHighlightLayer::initialize(VkContext* ctx, pipeline::AssetManager* am) {
|
||||
vkCtx_ = ctx;
|
||||
assetManager_ = am;
|
||||
}
|
||||
|
||||
void ZoneHighlightLayer::clearTextures() {
|
||||
if (vkCtx_ && !highlights_.empty()) {
|
||||
// Defer destruction until all in-flight frames complete.
|
||||
// The previous frame's command buffer may still reference these ImGui
|
||||
// descriptor sets and texture image views from highlight draw commands.
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
VmaAllocator alloc = vkCtx_->getAllocator();
|
||||
|
||||
struct DeferredHighlight {
|
||||
std::unique_ptr<VkTexture> texture;
|
||||
VkDescriptorSet imguiDS;
|
||||
};
|
||||
auto captured = std::make_shared<std::vector<DeferredHighlight>>();
|
||||
for (auto& [name, entry] : highlights_) {
|
||||
DeferredHighlight dh;
|
||||
dh.texture = std::move(entry.texture);
|
||||
dh.imguiDS = entry.imguiDS;
|
||||
captured->push_back(std::move(dh));
|
||||
}
|
||||
vkCtx_->deferAfterAllFrameFences([device, alloc, captured]() {
|
||||
for (auto& dh : *captured) {
|
||||
if (dh.imguiDS) ImGui_ImplVulkan_RemoveTexture(dh.imguiDS);
|
||||
if (dh.texture) dh.texture->destroy(device, alloc);
|
||||
}
|
||||
});
|
||||
}
|
||||
highlights_.clear();
|
||||
missingHighlights_.clear();
|
||||
}
|
||||
|
||||
void ZoneHighlightLayer::ensureHighlight(const std::string& key,
|
||||
const std::string& customPath) {
|
||||
if (!vkCtx_ || !assetManager_) return;
|
||||
if (key.empty()) return;
|
||||
if (highlights_.count(key) || missingHighlights_.count(key)) return;
|
||||
|
||||
// Determine BLP path
|
||||
std::string path;
|
||||
if (!customPath.empty()) {
|
||||
path = customPath;
|
||||
} else {
|
||||
std::string lower = key;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
path = "Interface\\WorldMap\\" + key + "\\" + lower + "highlight.blp";
|
||||
}
|
||||
|
||||
auto blpImage = assetManager_->loadTexture(path);
|
||||
if (!blpImage.isValid()) {
|
||||
LOG_WARNING("ZoneHighlightLayer: highlight not found for key='", key, "' path='", path, "'");
|
||||
missingHighlights_.insert(key);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("ZoneHighlightLayer: loaded highlight key='", key, "' path='", path,
|
||||
"' ", blpImage.width, "x", blpImage.height, " dataSize=", blpImage.data.size());
|
||||
|
||||
// WoW highlight BLPs with alphaDepth=0 use additive blending (white=glow, black=invisible).
|
||||
// Convert to alpha-blend compatible: set alpha = max(R,G,B) for fully opaque textures.
|
||||
{
|
||||
bool allOpaque = true;
|
||||
for (size_t i = 3; i < blpImage.data.size(); i += 4) {
|
||||
if (blpImage.data[i] < 255) { allOpaque = false; break; }
|
||||
}
|
||||
if (allOpaque) {
|
||||
for (size_t i = 0; i < blpImage.data.size(); i += 4) {
|
||||
uint8_t r = blpImage.data[i], g = blpImage.data[i + 1], b = blpImage.data[i + 2];
|
||||
blpImage.data[i + 3] = std::max({r, g, b});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
|
||||
auto tex = std::make_unique<VkTexture>();
|
||||
if (!tex->upload(*vkCtx_, blpImage.data.data(), blpImage.width, blpImage.height,
|
||||
VK_FORMAT_R8G8B8A8_UNORM, false)) {
|
||||
missingHighlights_.insert(key);
|
||||
return;
|
||||
}
|
||||
if (!tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f)) {
|
||||
tex->destroy(device, vkCtx_->getAllocator());
|
||||
missingHighlights_.insert(key);
|
||||
return;
|
||||
}
|
||||
|
||||
VkDescriptorSet ds = ImGui_ImplVulkan_AddTexture(
|
||||
tex->getSampler(), tex->getImageView(),
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
|
||||
if (!ds) {
|
||||
tex->destroy(device, vkCtx_->getAllocator());
|
||||
missingHighlights_.insert(key);
|
||||
return;
|
||||
}
|
||||
|
||||
HighlightEntry entry;
|
||||
entry.texture = std::move(tex);
|
||||
entry.imguiDS = ds;
|
||||
highlights_[key] = std::move(entry);
|
||||
}
|
||||
|
||||
ImTextureID ZoneHighlightLayer::getHighlightTexture(const std::string& key,
|
||||
const std::string& customPath) {
|
||||
ensureHighlight(key, customPath);
|
||||
auto it = highlights_.find(key);
|
||||
if (it != highlights_.end() && it->second.imguiDS) {
|
||||
return reinterpret_cast<ImTextureID>(it->second.imguiDS);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ZoneHighlightLayer::render(const LayerContext& ctx) {
|
||||
if (ctx.viewLevel != ViewLevel::CONTINENT || ctx.continentIdx < 0) return;
|
||||
if (!ctx.zones) return;
|
||||
|
||||
const auto& cont = (*ctx.zones)[ctx.continentIdx];
|
||||
float cLeft = cont.bounds.locLeft, cRight = cont.bounds.locRight;
|
||||
float cTop = cont.bounds.locTop, cBottom = cont.bounds.locBottom;
|
||||
getContinentProjectionBounds(*ctx.zones, ctx.continentIdx, cLeft, cRight, cTop, cBottom);
|
||||
float cDenomU = cLeft - cRight;
|
||||
float cDenomV = cTop - cBottom;
|
||||
|
||||
if (std::abs(cDenomU) < 0.001f || std::abs(cDenomV) < 0.001f) return;
|
||||
|
||||
hoveredZone_ = -1;
|
||||
ImVec2 mousePos = ImGui::GetMousePos();
|
||||
|
||||
// ── Render zone rectangles using DBC world-coord AABB projection ──
|
||||
// (Restored from old WorldMap::renderImGuiOverlay — no ZMP dependency)
|
||||
for (int zi = 0; zi < static_cast<int>(ctx.zones->size()); zi++) {
|
||||
if (!zoneBelongsToContinent(*ctx.zones, zi, ctx.continentIdx)) continue;
|
||||
const auto& z = (*ctx.zones)[zi];
|
||||
if (std::abs(z.bounds.locLeft - z.bounds.locRight) < 0.001f ||
|
||||
std::abs(z.bounds.locTop - z.bounds.locBottom) < 0.001f) continue;
|
||||
|
||||
// Project from WorldMapArea.dbc world coords
|
||||
float zuMin = (cLeft - z.bounds.locLeft) / cDenomU;
|
||||
float zuMax = (cLeft - z.bounds.locRight) / cDenomU;
|
||||
float zvMin = (cTop - z.bounds.locTop) / cDenomV;
|
||||
float zvMax = (cTop - z.bounds.locBottom) / cDenomV;
|
||||
|
||||
constexpr float kOverlayShrink = 0.92f;
|
||||
float cu = (zuMin + zuMax) * 0.5f, cv = (zvMin + zvMax) * 0.5f;
|
||||
float hu = (zuMax - zuMin) * 0.5f * kOverlayShrink;
|
||||
float hv = (zvMax - zvMin) * 0.5f * kOverlayShrink;
|
||||
zuMin = cu - hu; zuMax = cu + hu;
|
||||
zvMin = cv - hv; zvMax = cv + hv;
|
||||
|
||||
constexpr float kVOffset = -0.15f;
|
||||
zvMin = (zvMin - 0.5f) + 0.5f + kVOffset;
|
||||
zvMax = (zvMax - 0.5f) + 0.5f + kVOffset;
|
||||
|
||||
zuMin = std::clamp(zuMin, 0.0f, 1.0f);
|
||||
zuMax = std::clamp(zuMax, 0.0f, 1.0f);
|
||||
zvMin = std::clamp(zvMin, 0.0f, 1.0f);
|
||||
zvMax = std::clamp(zvMax, 0.0f, 1.0f);
|
||||
if (zuMax - zuMin < 0.001f || zvMax - zvMin < 0.001f) continue;
|
||||
|
||||
float sx0 = ctx.imgMin.x + zuMin * ctx.displayW;
|
||||
float sy0 = ctx.imgMin.y + zvMin * ctx.displayH;
|
||||
float sx1 = ctx.imgMin.x + zuMax * ctx.displayW;
|
||||
float sy1 = ctx.imgMin.y + zvMax * ctx.displayH;
|
||||
|
||||
bool explored = !ctx.exploredZones ||
|
||||
ctx.exploredZones->empty() ||
|
||||
ctx.exploredZones->count(zi) > 0;
|
||||
bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 &&
|
||||
mousePos.y >= sy0 && mousePos.y <= sy1);
|
||||
|
||||
if (hovered) {
|
||||
hoveredZone_ = zi;
|
||||
|
||||
if (prevHoveredZone_ == zi) {
|
||||
hoverHighlightAlpha_ = std::min(hoverHighlightAlpha_ + 0.08f, 1.0f);
|
||||
} else {
|
||||
hoverHighlightAlpha_ = 0.3f;
|
||||
}
|
||||
|
||||
// Draw the highlight BLP texture within the zone's bounding rectangle.
|
||||
auto it = highlights_.find(z.areaName);
|
||||
if (it == highlights_.end()) ensureHighlight(z.areaName, "");
|
||||
it = highlights_.find(z.areaName);
|
||||
if (it != highlights_.end() && it->second.imguiDS) {
|
||||
uint8_t imgAlpha = static_cast<uint8_t>(255.0f * hoverHighlightAlpha_);
|
||||
// Draw twice for a very bright glow effect
|
||||
ctx.drawList->AddImage(
|
||||
reinterpret_cast<ImTextureID>(it->second.imguiDS),
|
||||
ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||
ImVec2(0, 0), ImVec2(1, 1),
|
||||
IM_COL32(255, 255, 255, imgAlpha));
|
||||
ctx.drawList->AddImage(
|
||||
reinterpret_cast<ImTextureID>(it->second.imguiDS),
|
||||
ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||
ImVec2(0, 0), ImVec2(1, 1),
|
||||
IM_COL32(255, 255, 200, imgAlpha));
|
||||
} else {
|
||||
// Fallback: bright colored rectangle if no highlight texture
|
||||
uint8_t fillAlpha = static_cast<uint8_t>(100.0f * hoverHighlightAlpha_);
|
||||
ctx.drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||
IM_COL32(255, 235, 50, fillAlpha));
|
||||
}
|
||||
|
||||
uint8_t borderAlpha = static_cast<uint8_t>(200.0f * hoverHighlightAlpha_);
|
||||
ctx.drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||
IM_COL32(255, 225, 50, borderAlpha), 0, 0, 2.0f);
|
||||
} else if (explored) {
|
||||
ctx.drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||
IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f);
|
||||
}
|
||||
|
||||
// Zone name label
|
||||
bool zoneExplored = explored;
|
||||
if (!z.areaName.empty()) {
|
||||
const ZoneMeta* meta = metadata_ ? metadata_->find(z.areaName) : nullptr;
|
||||
std::string label = ZoneMetadata::formatLabel(z.areaName, meta);
|
||||
|
||||
ImFont* font = ImGui::GetFont();
|
||||
float fontSize = ImGui::GetFontSize() * 0.75f;
|
||||
ImVec2 labelSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, label.c_str());
|
||||
float zoneCx = (sx0 + sx1) * 0.5f;
|
||||
float zoneCy = (sy0 + sy1) * 0.5f;
|
||||
float lx = zoneCx - labelSz.x * 0.5f;
|
||||
float ly = zoneCy - labelSz.y * 0.5f;
|
||||
|
||||
if (labelSz.x < (sx1 - sx0) * 1.1f && labelSz.y < (sy1 - sy0) * 0.8f) {
|
||||
ImU32 textColor;
|
||||
if (!zoneExplored) {
|
||||
textColor = IM_COL32(140, 140, 140, 130);
|
||||
} else if (meta) {
|
||||
switch (meta->faction) {
|
||||
case ZoneFaction::Alliance: textColor = IM_COL32(100, 160, 255, 200); break;
|
||||
case ZoneFaction::Horde: textColor = IM_COL32(255, 100, 100, 200); break;
|
||||
case ZoneFaction::Contested: textColor = IM_COL32(255, 215, 0, 190); break;
|
||||
default: textColor = IM_COL32(255, 230, 180, 180); break;
|
||||
}
|
||||
} else {
|
||||
textColor = IM_COL32(255, 230, 180, 180);
|
||||
}
|
||||
ctx.drawList->AddText(font, fontSize,
|
||||
ImVec2(lx + 1.0f, ly + 1.0f),
|
||||
IM_COL32(0, 0, 0, 140), label.c_str());
|
||||
ctx.drawList->AddText(font, fontSize,
|
||||
ImVec2(lx, ly), textColor, label.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevHoveredZone_ = hoveredZone_;
|
||||
if (hoveredZone_ < 0) {
|
||||
hoverHighlightAlpha_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
206
src/rendering/world_map/map_resolver.cpp
Normal file
206
src/rendering/world_map/map_resolver.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
// map_resolver.cpp — Centralized map navigation resolution for the world map.
|
||||
// Map folder names resolved from a built-in table matching
|
||||
// Data/interface/worldmap/ — no dependency on WorldLoader.
|
||||
#include "rendering/world_map/map_resolver.hpp"
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
// ── Worldmap folder table (from Data/interface/worldmap/) ────
|
||||
// Each entry maps a DBC MapID to its worldmap folder name and UI display name.
|
||||
// Folder names match the directories under Data/interface/worldmap/.
|
||||
|
||||
struct MapFolderEntry {
|
||||
uint32_t mapId;
|
||||
const char* folder; // worldmap folder name (case as on disk)
|
||||
const char* displayName; // UI display name
|
||||
};
|
||||
|
||||
static constexpr MapFolderEntry kMapFolders[] = {
|
||||
// Special UI-only views (no DBC MapID — sentinel values)
|
||||
{ UINT32_MAX, "World", "World" },
|
||||
{ UINT32_MAX - 1, "Cosmic", "Cosmic" },
|
||||
// Continents
|
||||
{ 0, "Azeroth", "Eastern Kingdoms" },
|
||||
{ 1, "Kalimdor", "Kalimdor" },
|
||||
{ 530, "Expansion01", "Outland" },
|
||||
{ 571, "Northrend", "Northrend" },
|
||||
// Dungeons / instances with worldmap folders
|
||||
// (Data/interface/worldmap/<folder>/ exists for these)
|
||||
{ 33, "Shadowfang", "Shadowfang Keep" }, // placeholder – no folder yet
|
||||
{ 209, "Tanaris", "Tanaris" }, // shared with zone
|
||||
{ 534, "CoTStratholme", "Caverns of Time" },
|
||||
{ 574, "UtgardeKeep", "Utgarde Keep" },
|
||||
{ 575, "UtgardePinnacle", "Utgarde Pinnacle" },
|
||||
{ 578, "Nexus80", "The Nexus" },
|
||||
{ 595, "ThecullingOfStratholme","Culling of Stratholme" },
|
||||
{ 599, "HallsOfLightning", "Halls of Lightning" },
|
||||
{ 600, "HallsOfStone", "Halls of Stone" },
|
||||
{ 601, "DrakTheron", "Drak'Theron Keep" },
|
||||
{ 602, "GunDrak", "Gundrak" },
|
||||
{ 603, "Ulduar77", "Ulduar" },
|
||||
{ 608, "VioletHold", "Violet Hold" },
|
||||
{ 619, "AhnKahet", "Ahn'kahet" },
|
||||
{ 631, "IcecrownCitadel", "Icecrown Citadel" },
|
||||
{ 632, "TheForgeOfSouls", "Forge of Souls" },
|
||||
{ 649, "TheArgentColiseum", "Trial of the Crusader" },
|
||||
{ 658, "PitOfSaron", "Pit of Saron" },
|
||||
{ 668, "HallsOfReflection", "Halls of Reflection" },
|
||||
{ 724, "TheRubySanctum", "Ruby Sanctum" },
|
||||
};
|
||||
|
||||
static constexpr int kMapFolderCount = sizeof(kMapFolders) / sizeof(kMapFolders[0]);
|
||||
|
||||
// ── Map folder lookup functions ──────────────────────────────
|
||||
|
||||
const char* mapIdToFolder(uint32_t mapId) {
|
||||
for (int i = 0; i < kMapFolderCount; i++) {
|
||||
if (kMapFolders[i].mapId == mapId)
|
||||
return kMapFolders[i].folder;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
int folderToMapId(const std::string& folder) {
|
||||
for (int i = 0; i < kMapFolderCount; i++) {
|
||||
// Case-insensitive compare
|
||||
const char* entry = kMapFolders[i].folder;
|
||||
if (folder.size() != std::char_traits<char>::length(entry)) continue;
|
||||
bool match = true;
|
||||
for (size_t j = 0; j < folder.size(); j++) {
|
||||
if (std::tolower(static_cast<unsigned char>(folder[j])) !=
|
||||
std::tolower(static_cast<unsigned char>(entry[j]))) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return static_cast<int>(kMapFolders[i].mapId);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* mapDisplayName(uint32_t mapId) {
|
||||
for (int i = 0; i < kMapFolderCount; i++) {
|
||||
if (kMapFolders[i].mapId == mapId)
|
||||
return kMapFolders[i].displayName;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ── Helper: find best continent zone for a mapId ─────────────
|
||||
|
||||
int findContinentForMapId(const std::vector<Zone>& zones,
|
||||
uint32_t mapId,
|
||||
int cosmicIdx) {
|
||||
// 1) Prefer a leaf continent whose displayMapID matches the target mapId.
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
if (i == cosmicIdx) continue;
|
||||
if (zones[i].areaID != 0) continue;
|
||||
if (isLeafContinent(zones, i) && zones[i].displayMapID == mapId)
|
||||
return i;
|
||||
}
|
||||
|
||||
// 2) Find the first non-root, non-cosmic continent.
|
||||
int firstContinent = -1;
|
||||
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
||||
if (i == cosmicIdx) continue;
|
||||
if (zones[i].areaID != 0) continue;
|
||||
if (firstContinent < 0) firstContinent = i;
|
||||
if (!isRootContinent(zones, i)) return i;
|
||||
}
|
||||
|
||||
// 3) Fallback to first continent entry
|
||||
return firstContinent;
|
||||
}
|
||||
|
||||
// ── Resolve WORLD view region click ──────────────────────────
|
||||
|
||||
MapResolveResult resolveWorldRegionClick(uint32_t regionMapId,
|
||||
const std::vector<Zone>& zones,
|
||||
int currentMapId,
|
||||
int cosmicIdx) {
|
||||
MapResolveResult result;
|
||||
|
||||
if (static_cast<int>(regionMapId) == currentMapId) {
|
||||
// Target map is already loaded — navigate to the matching continent
|
||||
// within the current zone data (no reload needed).
|
||||
int contIdx = findContinentForMapId(zones, regionMapId, cosmicIdx);
|
||||
if (contIdx >= 0) {
|
||||
result.action = MapResolveAction::NAVIGATE_CONTINENT;
|
||||
result.targetZoneIdx = contIdx;
|
||||
LOG_INFO("resolveWorldRegionClick: mapId=", regionMapId,
|
||||
" matches current map — NAVIGATE_CONTINENT idx=", contIdx);
|
||||
} else {
|
||||
LOG_WARNING("resolveWorldRegionClick: mapId=", regionMapId,
|
||||
" matches current but no continent found");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Different map — need to load it
|
||||
const char* folder = mapIdToFolder(regionMapId);
|
||||
if (folder[0]) {
|
||||
result.action = MapResolveAction::LOAD_MAP;
|
||||
result.targetMapName = folder;
|
||||
LOG_INFO("resolveWorldRegionClick: mapId=", regionMapId,
|
||||
" → LOAD_MAP '", folder, "'");
|
||||
} else {
|
||||
LOG_WARNING("resolveWorldRegionClick: unknown mapId=", regionMapId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Resolve CONTINENT view zone click ────────────────────────
|
||||
|
||||
MapResolveResult resolveZoneClick(int zoneIdx,
|
||||
const std::vector<Zone>& zones,
|
||||
int currentMapId) {
|
||||
MapResolveResult result;
|
||||
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return result;
|
||||
|
||||
const auto& zone = zones[zoneIdx];
|
||||
|
||||
// If the zone's displayMapID differs from the current map, it belongs to
|
||||
// a different continent/map. Load that map instead.
|
||||
// Skip sentinel values (UINT32_MAX / UINT32_MAX-1) used by kMapFolders for
|
||||
// World/Cosmic; the DBC stores -1 (0xFFFFFFFF) to mean "no display map".
|
||||
if (zone.displayMapID != 0 &&
|
||||
zone.displayMapID < UINT32_MAX - 1 &&
|
||||
static_cast<int>(zone.displayMapID) != currentMapId) {
|
||||
const char* folder = mapIdToFolder(zone.displayMapID);
|
||||
if (folder[0]) {
|
||||
result.action = MapResolveAction::LOAD_MAP;
|
||||
result.targetMapName = folder;
|
||||
LOG_INFO("resolveZoneClick: zone[", zoneIdx, "] '", zone.areaName,
|
||||
"' displayMapID=", zone.displayMapID, " → LOAD_MAP '", folder, "'");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Normal case: enter the zone within the current map
|
||||
result.action = MapResolveAction::ENTER_ZONE;
|
||||
result.targetZoneIdx = zoneIdx;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Resolve COSMIC view click ────────────────────────────────
|
||||
|
||||
MapResolveResult resolveCosmicClick(uint32_t targetMapId) {
|
||||
MapResolveResult result;
|
||||
const char* folder = mapIdToFolder(targetMapId);
|
||||
if (folder[0]) {
|
||||
result.action = MapResolveAction::LOAD_MAP;
|
||||
result.targetMapName = folder;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
21
src/rendering/world_map/overlay_renderer.cpp
Normal file
21
src/rendering/world_map/overlay_renderer.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// overlay_renderer.cpp — ImGui overlay orchestrator for the world map.
|
||||
// Extracted from WorldMap::renderImGuiOverlay (Phase 8 of refactoring plan).
|
||||
#include "rendering/world_map/overlay_renderer.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void OverlayRenderer::addLayer(std::unique_ptr<IOverlayLayer> layer) {
|
||||
layers_.push_back(std::move(layer));
|
||||
}
|
||||
|
||||
void OverlayRenderer::render(const LayerContext& ctx) {
|
||||
for (auto& layer : layers_) {
|
||||
layer->render(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
141
src/rendering/world_map/view_state_machine.cpp
Normal file
141
src/rendering/world_map/view_state_machine.cpp
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// view_state_machine.cpp — Navigation state and transitions for the world map.
|
||||
// Extracted from WorldMap::zoomIn, zoomOut, enterWorldView, enterCosmicView
|
||||
// (Phase 6 of refactoring plan).
|
||||
#include "rendering/world_map/view_state_machine.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void ViewStateMachine::startTransition(ViewLevel from, ViewLevel to, float duration) {
|
||||
transition_.active = true;
|
||||
transition_.progress = 0.0f;
|
||||
transition_.duration = duration;
|
||||
transition_.fromLevel = from;
|
||||
transition_.toLevel = to;
|
||||
}
|
||||
|
||||
bool ViewStateMachine::updateTransition(float deltaTime) {
|
||||
if (!transition_.active) return false;
|
||||
transition_.progress += deltaTime / transition_.duration;
|
||||
if (transition_.progress >= 1.0f) {
|
||||
transition_.progress = 1.0f;
|
||||
transition_.active = false;
|
||||
}
|
||||
return transition_.active;
|
||||
}
|
||||
|
||||
ViewStateMachine::ZoomResult ViewStateMachine::zoomIn(int hoveredZoneIdx, int playerZoneIdx) {
|
||||
ZoomResult result;
|
||||
|
||||
if (level_ == ViewLevel::COSMIC) {
|
||||
startTransition(ViewLevel::COSMIC, ViewLevel::WORLD);
|
||||
level_ = ViewLevel::WORLD;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::WORLD;
|
||||
// Caller should call enterWorldView() to determine target index
|
||||
return result;
|
||||
}
|
||||
|
||||
if (level_ == ViewLevel::WORLD) {
|
||||
if (continentIdx_ >= 0) {
|
||||
startTransition(ViewLevel::WORLD, ViewLevel::CONTINENT);
|
||||
level_ = ViewLevel::CONTINENT;
|
||||
currentIdx_ = continentIdx_;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::CONTINENT;
|
||||
result.targetIdx = continentIdx_;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (level_ == ViewLevel::CONTINENT) {
|
||||
// Prefer the zone the mouse is hovering over; fall back to the player's zone
|
||||
int zoneIdx = hoveredZoneIdx >= 0 ? hoveredZoneIdx : playerZoneIdx;
|
||||
if (zoneIdx >= 0) {
|
||||
startTransition(ViewLevel::CONTINENT, ViewLevel::ZONE);
|
||||
level_ = ViewLevel::ZONE;
|
||||
currentIdx_ = zoneIdx;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::ZONE;
|
||||
result.targetIdx = zoneIdx;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ViewStateMachine::ZoomResult ViewStateMachine::zoomOut() {
|
||||
ZoomResult result;
|
||||
|
||||
if (level_ == ViewLevel::ZONE) {
|
||||
if (continentIdx_ >= 0) {
|
||||
startTransition(ViewLevel::ZONE, ViewLevel::CONTINENT);
|
||||
level_ = ViewLevel::CONTINENT;
|
||||
currentIdx_ = continentIdx_;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::CONTINENT;
|
||||
result.targetIdx = continentIdx_;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (level_ == ViewLevel::CONTINENT) {
|
||||
startTransition(ViewLevel::CONTINENT, ViewLevel::WORLD);
|
||||
level_ = ViewLevel::WORLD;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::WORLD;
|
||||
// Caller should call enterWorldView() to determine target index
|
||||
return result;
|
||||
}
|
||||
|
||||
if (level_ == ViewLevel::WORLD) {
|
||||
// Vanilla: cosmic view disabled, don't zoom out further
|
||||
if (!cosmicEnabled_) return result;
|
||||
startTransition(ViewLevel::WORLD, ViewLevel::COSMIC);
|
||||
level_ = ViewLevel::COSMIC;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::COSMIC;
|
||||
// Caller should call enterCosmicView() to determine target index
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ViewStateMachine::ZoomResult ViewStateMachine::enterWorldView() {
|
||||
ZoomResult result;
|
||||
level_ = ViewLevel::WORLD;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::WORLD;
|
||||
// Caller is responsible for finding the root continent and compositing
|
||||
return result;
|
||||
}
|
||||
|
||||
ViewStateMachine::ZoomResult ViewStateMachine::enterCosmicView() {
|
||||
// Vanilla: cosmic view is disabled — stay in world view
|
||||
if (!cosmicEnabled_) {
|
||||
return enterWorldView();
|
||||
}
|
||||
|
||||
ZoomResult result;
|
||||
level_ = ViewLevel::COSMIC;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::COSMIC;
|
||||
// Caller uses cosmicIdx from DataRepository
|
||||
return result;
|
||||
}
|
||||
|
||||
ViewStateMachine::ZoomResult ViewStateMachine::enterZone(int zoneIdx) {
|
||||
ZoomResult result;
|
||||
startTransition(ViewLevel::CONTINENT, ViewLevel::ZONE);
|
||||
level_ = ViewLevel::ZONE;
|
||||
currentIdx_ = zoneIdx;
|
||||
result.changed = true;
|
||||
result.newLevel = ViewLevel::ZONE;
|
||||
result.targetIdx = zoneIdx;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
1060
src/rendering/world_map/world_map_facade.cpp
Normal file
1060
src/rendering/world_map/world_map_facade.cpp
Normal file
File diff suppressed because it is too large
Load diff
118
src/rendering/world_map/zone_metadata.cpp
Normal file
118
src/rendering/world_map/zone_metadata.cpp
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// zone_metadata.cpp — Zone level ranges, faction data, and label formatting.
|
||||
// Extracted from WorldMap::initZoneMeta (Phase 4 of refactoring plan).
|
||||
#include "rendering/world_map/zone_metadata.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
namespace world_map {
|
||||
|
||||
void ZoneMetadata::initialize() {
|
||||
if (!table_.empty()) return;
|
||||
|
||||
// Populate known zone level ranges and faction alignment.
|
||||
// This covers major open-world zones for Vanilla/TBC/WotLK.
|
||||
auto add = [this](const char* name, uint8_t lo, uint8_t hi, ZoneFaction f) {
|
||||
table_[name] = {lo, hi, f};
|
||||
};
|
||||
|
||||
// === Eastern Kingdoms ===
|
||||
add("Elwynn", 1, 10, ZoneFaction::Alliance);
|
||||
add("DunMorogh", 1, 10, ZoneFaction::Alliance);
|
||||
add("TirisfalGlades", 1, 10, ZoneFaction::Horde);
|
||||
add("Westfall", 10, 20, ZoneFaction::Alliance);
|
||||
add("LochModan", 10, 20, ZoneFaction::Alliance);
|
||||
add("Silverpine", 10, 20, ZoneFaction::Horde);
|
||||
add("Redridge", 15, 25, ZoneFaction::Contested);
|
||||
add("Duskwood", 18, 30, ZoneFaction::Alliance);
|
||||
add("Wetlands", 20, 30, ZoneFaction::Alliance);
|
||||
add("Hillsbrad", 20, 30, ZoneFaction::Contested);
|
||||
add("Alterac", 30, 40, ZoneFaction::Contested);
|
||||
add("Arathi", 30, 40, ZoneFaction::Contested);
|
||||
add("StranglethornVale",30, 45, ZoneFaction::Contested);
|
||||
add("Stranglethorn", 30, 45, ZoneFaction::Contested);
|
||||
add("Badlands", 35, 45, ZoneFaction::Contested);
|
||||
add("SwampOfSorrows", 35, 45, ZoneFaction::Contested);
|
||||
add("TheBlastedLands", 45, 55, ZoneFaction::Contested);
|
||||
add("SearingGorge", 43, 50, ZoneFaction::Contested);
|
||||
add("BurningSteppes", 50, 58, ZoneFaction::Contested);
|
||||
add("WesternPlaguelands",51,58, ZoneFaction::Contested);
|
||||
add("EasternPlaguelands",53,60, ZoneFaction::Contested);
|
||||
add("Hinterlands", 40, 50, ZoneFaction::Contested);
|
||||
add("DeadwindPass", 55, 60, ZoneFaction::Contested);
|
||||
|
||||
// === Kalimdor ===
|
||||
add("Durotar", 1, 10, ZoneFaction::Horde);
|
||||
add("Mulgore", 1, 10, ZoneFaction::Horde);
|
||||
add("Teldrassil", 1, 10, ZoneFaction::Alliance);
|
||||
add("Darkshore", 10, 20, ZoneFaction::Alliance);
|
||||
add("Barrens", 10, 25, ZoneFaction::Horde);
|
||||
add("Ashenvale", 18, 30, ZoneFaction::Contested);
|
||||
add("StonetalonMountains",15,27,ZoneFaction::Contested);
|
||||
add("ThousandNeedles", 25, 35, ZoneFaction::Contested);
|
||||
add("Desolace", 30, 40, ZoneFaction::Contested);
|
||||
add("Dustwallow", 35, 45, ZoneFaction::Contested);
|
||||
add("Feralas", 40, 50, ZoneFaction::Contested);
|
||||
add("Tanaris", 40, 50, ZoneFaction::Contested);
|
||||
add("Azshara", 45, 55, ZoneFaction::Contested);
|
||||
add("UngoroCrater", 48, 55, ZoneFaction::Contested);
|
||||
add("Felwood", 48, 55, ZoneFaction::Contested);
|
||||
add("Winterspring", 55, 60, ZoneFaction::Contested);
|
||||
add("Silithus", 55, 60, ZoneFaction::Contested);
|
||||
add("Moonglade", 55, 60, ZoneFaction::Contested);
|
||||
|
||||
// === TBC: Outland ===
|
||||
add("HellFire", 58, 63, ZoneFaction::Contested);
|
||||
add("Zangarmarsh", 60, 64, ZoneFaction::Contested);
|
||||
add("TerokkarForest", 62, 65, ZoneFaction::Contested);
|
||||
add("Nagrand", 64, 67, ZoneFaction::Contested);
|
||||
add("BladesEdgeMountains",65,68,ZoneFaction::Contested);
|
||||
add("Netherstorm", 67, 70, ZoneFaction::Contested);
|
||||
add("ShadowmoonValley",67, 70, ZoneFaction::Contested);
|
||||
|
||||
// === WotLK: Northrend ===
|
||||
add("BoreanTundra", 68, 72, ZoneFaction::Contested);
|
||||
add("HowlingFjord", 68, 72, ZoneFaction::Contested);
|
||||
add("Dragonblight", 71, 75, ZoneFaction::Contested);
|
||||
add("GrizzlyHills", 73, 75, ZoneFaction::Contested);
|
||||
add("ZulDrak", 74, 77, ZoneFaction::Contested);
|
||||
add("SholazarBasin", 76, 78, ZoneFaction::Contested);
|
||||
add("StormPeaks", 77, 80, ZoneFaction::Contested);
|
||||
add("Icecrown", 77, 80, ZoneFaction::Contested);
|
||||
add("CrystalsongForest",77,80, ZoneFaction::Contested);
|
||||
add("LakeWintergrasp", 77, 80, ZoneFaction::Contested);
|
||||
}
|
||||
|
||||
const ZoneMeta* ZoneMetadata::find(const std::string& areaName) const {
|
||||
auto it = table_.find(areaName);
|
||||
return it != table_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
std::string ZoneMetadata::formatLabel(const std::string& areaName,
|
||||
const ZoneMeta* meta) {
|
||||
std::string label = areaName;
|
||||
if (meta) {
|
||||
if (meta->minLevel > 0 && meta->maxLevel > 0) {
|
||||
label += " (" + std::to_string(meta->minLevel) + "-" +
|
||||
std::to_string(meta->maxLevel) + ")";
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
std::string ZoneMetadata::formatHoverLabel(const std::string& areaName,
|
||||
const ZoneMeta* meta) {
|
||||
std::string label = formatLabel(areaName, meta);
|
||||
if (meta) {
|
||||
switch (meta->faction) {
|
||||
case ZoneFaction::Alliance: label += " [Alliance]"; break;
|
||||
case ZoneFaction::Horde: label += " [Horde]"; break;
|
||||
case ZoneFaction::Contested: label += " [Contested]"; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace world_map
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
#include "ui/quest_log_screen.hpp"
|
||||
#include "ui/ui_colors.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/world_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
|
|
@ -611,13 +612,8 @@ void ActionBarPanel::renderActionBar(game::GameHandler& gameHandler,
|
|||
}
|
||||
// Fall back to continent name if zone unavailable
|
||||
if (homeLocation.empty()) {
|
||||
switch (mapId) {
|
||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
||||
case 1: homeLocation = "Kalimdor"; break;
|
||||
case 530: homeLocation = "Outland"; break;
|
||||
case 571: homeLocation = "Northrend"; break;
|
||||
default: homeLocation = "Unknown"; break;
|
||||
}
|
||||
const char* dn = core::WorldLoader::mapDisplayName(mapId);
|
||||
homeLocation = dn ? dn : "Unknown";
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
||||
"Home: %s", homeLocation.c_str());
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "ui/keybinding_manager.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/world_loader.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
|
|
@ -2632,15 +2633,8 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
homeLocation = gameHandler_->getWhoAreaName(zoneId);
|
||||
// Fall back to continent name if zone unavailable
|
||||
if (homeLocation.empty()) {
|
||||
switch (mapId) {
|
||||
case 0: homeLocation = "Eastern Kingdoms"; break;
|
||||
case 1: homeLocation = "Kalimdor"; break;
|
||||
case 530: homeLocation = "Outland"; break;
|
||||
case 571: homeLocation = "Northrend"; break;
|
||||
case 13: homeLocation = "Test"; break;
|
||||
case 169: homeLocation = "Emerald Dream"; break;
|
||||
default: homeLocation = "Unknown"; break;
|
||||
}
|
||||
const char* dn = core::WorldLoader::mapDisplayName(mapId);
|
||||
homeLocation = dn ? dn : "Unknown";
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", homeLocation.c_str());
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue