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
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue