mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 15:50:20 +00:00
1141 lines
44 KiB
C++
1141 lines
44 KiB
C++
#include "rendering/world_map.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 "pipeline/dbc_layout.hpp"
|
|
#include "core/coordinates.hpp"
|
|
#include "core/input.hpp"
|
|
#include "core/logger.hpp"
|
|
#include <imgui.h>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <limits>
|
|
|
|
namespace wowee {
|
|
namespace rendering {
|
|
|
|
namespace {
|
|
bool isRootContinent(const std::vector<WorldMapZone>& 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<WorldMapZone>& 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;
|
|
}
|
|
} // namespace
|
|
|
|
// Push constant for world map tile composite vertex shader
|
|
struct WorldMapTilePush {
|
|
glm::vec2 gridOffset; // 8 bytes
|
|
float gridCols; // 4 bytes
|
|
float gridRows; // 4 bytes
|
|
}; // 16 bytes
|
|
|
|
WorldMap::WorldMap() = default;
|
|
|
|
WorldMap::~WorldMap() {
|
|
shutdown();
|
|
}
|
|
|
|
bool WorldMap::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("WorldMap: 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 (24 tile + 1 display = 25) ---
|
|
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: 12*2 tile + 1 display = 25 ---
|
|
constexpr uint32_t totalSets = 25;
|
|
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();
|
|
|
|
VkDescriptorSet allSets[25];
|
|
vkAllocateDescriptorSets(device, &allocInfo, allSets);
|
|
|
|
for (int f = 0; f < 2; f++)
|
|
for (int t = 0; t < 12; t++)
|
|
tileDescSets[f][t] = allSets[f * 12 + t];
|
|
imguiDisplaySet = allSets[24];
|
|
|
|
// --- 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("WorldMap: 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);
|
|
|
|
vs.destroy();
|
|
fs.destroy();
|
|
}
|
|
|
|
if (!tilePipeline) {
|
|
LOG_ERROR("WorldMap: failed to create tile pipeline");
|
|
return false;
|
|
}
|
|
|
|
initialized = true;
|
|
LOG_INFO("WorldMap initialized (", FBO_W, "x", FBO_H, " composite)");
|
|
return true;
|
|
}
|
|
|
|
void WorldMap::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 (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; }
|
|
|
|
destroyZoneTextures();
|
|
|
|
if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); }
|
|
|
|
zones.clear();
|
|
initialized = false;
|
|
vkCtx = nullptr;
|
|
}
|
|
|
|
void WorldMap::destroyZoneTextures() {
|
|
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& zone : zones) {
|
|
for (auto& tex : zone.tileTextures) tex = nullptr;
|
|
zone.tilesLoaded = false;
|
|
}
|
|
}
|
|
|
|
void WorldMap::setMapName(const std::string& name) {
|
|
if (mapName == name && !zones.empty()) return;
|
|
mapName = name;
|
|
|
|
destroyZoneTextures();
|
|
zones.clear();
|
|
continentIdx = -1;
|
|
currentIdx = -1;
|
|
compositedIdx = -1;
|
|
pendingCompositeIdx = -1;
|
|
viewLevel = ViewLevel::WORLD;
|
|
}
|
|
|
|
void WorldMap::setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData) {
|
|
if (!hasData || masks.empty()) {
|
|
hasServerExplorationMask = false;
|
|
serverExplorationMask.clear();
|
|
return;
|
|
}
|
|
hasServerExplorationMask = true;
|
|
serverExplorationMask = masks;
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// DBC zone loading (identical to GL version)
|
|
// --------------------------------------------------------
|
|
|
|
void WorldMap::loadZonesFromDBC() {
|
|
if (!zones.empty() || !assetManager) 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("WorldMap: Map.dbc '", mapName, "' -> mapID=", mapID);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mapID < 0) {
|
|
if (mapName == "Azeroth") mapID = 0;
|
|
else if (mapName == "Kalimdor") mapID = 1;
|
|
else if (mapName == "Expansion01") mapID = 530;
|
|
else if (mapName == "Northrend") mapID = 571;
|
|
else {
|
|
LOG_WARNING("WorldMap: unknown map '", mapName, "'");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr;
|
|
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
|
auto areaDbc = assetManager->loadDBC("AreaTable.dbc");
|
|
if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) {
|
|
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);
|
|
if (areaId != 0) exploreFlagByAreaId[areaId] = exploreFlag;
|
|
}
|
|
}
|
|
|
|
auto wmaDbc = assetManager->loadDBC("WorldMapArea.dbc");
|
|
if (!wmaDbc || !wmaDbc->isLoaded()) {
|
|
LOG_WARNING("WorldMap: WorldMapArea.dbc not found");
|
|
return;
|
|
}
|
|
|
|
const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr;
|
|
|
|
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;
|
|
|
|
WorldMapZone 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.locLeft = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocLeft"] : 4);
|
|
zone.locRight = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocRight"] : 5);
|
|
zone.locTop = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocTop"] : 6);
|
|
zone.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);
|
|
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
|
|
if (exploreIt != exploreFlagByAreaId.end())
|
|
zone.exploreFlag = exploreIt->second;
|
|
|
|
int idx = static_cast<int>(zones.size());
|
|
|
|
LOG_INFO("WorldMap: zone[", idx, "] areaID=", zone.areaID,
|
|
" '", zone.areaName, "' L=", zone.locLeft,
|
|
" R=", zone.locRight, " T=", zone.locTop,
|
|
" B=", zone.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.locLeft) > 0.001f || std::abs(cont.locRight) > 0.001f ||
|
|
std::abs(cont.locTop) > 0.001f || std::abs(cont.locBottom) > 0.001f)
|
|
continue;
|
|
|
|
bool first = true;
|
|
for (const auto& z : zones) {
|
|
if (z.areaID == 0) continue;
|
|
if (std::abs(z.locLeft - z.locRight) < 0.001f ||
|
|
std::abs(z.locTop - z.locBottom) < 0.001f)
|
|
continue;
|
|
if (z.parentWorldMapID != 0 && cont.wmaID != 0 && z.parentWorldMapID != cont.wmaID)
|
|
continue;
|
|
|
|
if (first) {
|
|
cont.locLeft = z.locLeft; cont.locRight = z.locRight;
|
|
cont.locTop = z.locTop; cont.locBottom = z.locBottom;
|
|
first = false;
|
|
} else {
|
|
cont.locLeft = std::max(cont.locLeft, z.locLeft);
|
|
cont.locRight = std::min(cont.locRight, z.locRight);
|
|
cont.locTop = std::max(cont.locTop, z.locTop);
|
|
cont.locBottom = std::min(cont.locBottom, z.locBottom);
|
|
}
|
|
}
|
|
}
|
|
|
|
LOG_INFO("WorldMap: loaded ", zones.size(), " zones for mapID=", mapID,
|
|
", continentIdx=", continentIdx);
|
|
}
|
|
|
|
int WorldMap::findBestContinentForPlayer(const glm::vec3& playerRenderPos) const {
|
|
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 hasLeafContinent = false;
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
if (zones[i].areaID == 0 && !isRootContinent(zones, i)) {
|
|
hasLeafContinent = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
const auto& z = zones[i];
|
|
if (z.areaID != 0) continue;
|
|
if (hasLeafContinent && isRootContinent(zones, i)) continue;
|
|
|
|
float minX = std::min(z.locLeft, z.locRight);
|
|
float maxX = std::max(z.locLeft, z.locRight);
|
|
float minY = std::min(z.locTop, z.locBottom);
|
|
float maxY = std::max(z.locTop, z.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 WorldMap::findZoneForPlayer(const glm::vec3& playerRenderPos) const {
|
|
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.locLeft, z.locRight);
|
|
float maxX = std::max(z.locLeft, z.locRight);
|
|
float minY = std::min(z.locTop, z.locBottom);
|
|
float maxY = std::max(z.locTop, z.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;
|
|
}
|
|
|
|
bool WorldMap::zoneBelongsToContinent(int zoneIdx, int contIdx) const {
|
|
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;
|
|
|
|
if (z.parentWorldMapID != 0 && cont.wmaID != 0)
|
|
return z.parentWorldMapID == cont.wmaID;
|
|
|
|
auto rectMinX = [](const WorldMapZone& a) { return std::min(a.locLeft, a.locRight); };
|
|
auto rectMaxX = [](const WorldMapZone& a) { return std::max(a.locLeft, a.locRight); };
|
|
auto rectMinY = [](const WorldMapZone& a) { return std::min(a.locTop, a.locBottom); };
|
|
auto rectMaxY = [](const WorldMapZone& a) { return std::max(a.locTop, a.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.locLeft + z.locRight) * 0.5f;
|
|
float centerY = (z.locTop + z.locBottom) * 0.5f;
|
|
return centerX >= rectMinX(cont) && centerX <= rectMaxX(cont) &&
|
|
centerY >= rectMinY(cont) && centerY <= rectMaxY(cont);
|
|
}
|
|
|
|
bool WorldMap::getContinentProjectionBounds(int contIdx, float& left, float& right,
|
|
float& top, float& bottom) const {
|
|
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.locLeft - cont.locRight) > 0.001f &&
|
|
std::abs(cont.locTop - cont.locBottom) > 0.001f) {
|
|
left = cont.locLeft; right = cont.locRight;
|
|
top = cont.locTop; bottom = cont.locBottom;
|
|
return true;
|
|
}
|
|
|
|
std::vector<float> northEdges, southEdges, westEdges, eastEdges;
|
|
for (int zi = 0; zi < static_cast<int>(zones.size()); zi++) {
|
|
if (!zoneBelongsToContinent(zi, contIdx)) continue;
|
|
const auto& z = zones[zi];
|
|
if (std::abs(z.locLeft - z.locRight) < 0.001f ||
|
|
std::abs(z.locTop - z.locBottom) < 0.001f) continue;
|
|
northEdges.push_back(std::max(z.locLeft, z.locRight));
|
|
southEdges.push_back(std::min(z.locLeft, z.locRight));
|
|
westEdges.push_back(std::max(z.locTop, z.locBottom));
|
|
eastEdges.push_back(std::min(z.locTop, z.locBottom));
|
|
}
|
|
|
|
if (northEdges.size() < 3) {
|
|
left = cont.locLeft; right = cont.locRight;
|
|
top = cont.locTop; bottom = cont.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.locLeft; right = cont.locRight;
|
|
top = cont.locTop; bottom = cont.locBottom;
|
|
}
|
|
return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f;
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Per-zone texture loading (Vulkan)
|
|
// --------------------------------------------------------
|
|
|
|
void WorldMap::loadZoneTextures(int zoneIdx) {
|
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return;
|
|
auto& zone = zones[zoneIdx];
|
|
if (zone.tilesLoaded) return;
|
|
zone.tilesLoaded = true;
|
|
|
|
const std::string& folder = zone.areaName;
|
|
if (folder.empty()) return;
|
|
|
|
std::vector<std::string> candidateFolders;
|
|
candidateFolders.push_back(folder);
|
|
if (zone.areaID == 0 && mapName == "Azeroth") {
|
|
if (folder != "Azeroth") candidateFolders.push_back("Azeroth");
|
|
if (folder != "EasternKingdoms") candidateFolders.push_back("EasternKingdoms");
|
|
}
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
int loaded = 0;
|
|
|
|
for (int i = 0; i < 12; i++) {
|
|
pipeline::BLPImage blpImage;
|
|
bool found = false;
|
|
for (const auto& testFolder : candidateFolders) {
|
|
std::string path = "Interface\\WorldMap\\" + testFolder + "\\" +
|
|
testFolder + std::to_string(i + 1) + ".blp";
|
|
blpImage = assetManager->loadTexture(path);
|
|
if (blpImage.isValid()) { found = true; break; }
|
|
}
|
|
|
|
if (!found) {
|
|
zone.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);
|
|
|
|
zone.tileTextures[i] = tex.get();
|
|
zoneTextures.push_back(std::move(tex));
|
|
loaded++;
|
|
}
|
|
|
|
LOG_INFO("WorldMap: loaded ", loaded, "/12 tiles for '", folder, "'");
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Request composite (deferred to compositePass)
|
|
// --------------------------------------------------------
|
|
|
|
void WorldMap::requestComposite(int zoneIdx) {
|
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return;
|
|
pendingCompositeIdx = zoneIdx;
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Off-screen composite pass (call BEFORE main render pass)
|
|
// --------------------------------------------------------
|
|
|
|
void WorldMap::compositePass(VkCommandBuffer cmd) {
|
|
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;
|
|
|
|
const auto& zone = zones[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 = zone.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);
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipeline);
|
|
|
|
VkDeviceSize offset = 0;
|
|
vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset);
|
|
|
|
// Draw 4x3 tile grid
|
|
for (int i = 0; i < 12; i++) {
|
|
if (!zone.tileTextures[i] || !zone.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);
|
|
}
|
|
|
|
compositeTarget->endPass(cmd);
|
|
compositedIdx = zoneIdx;
|
|
}
|
|
|
|
void WorldMap::enterWorldView() {
|
|
viewLevel = ViewLevel::WORLD;
|
|
|
|
int rootIdx = -1;
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
if (isRootContinent(zones, i)) { rootIdx = i; break; }
|
|
}
|
|
|
|
if (rootIdx >= 0) {
|
|
loadZoneTextures(rootIdx);
|
|
bool hasAnyTile = false;
|
|
for (VkTexture* tex : zones[rootIdx].tileTextures) {
|
|
if (tex != nullptr) { hasAnyTile = true; break; }
|
|
}
|
|
if (hasAnyTile) {
|
|
requestComposite(rootIdx);
|
|
currentIdx = rootIdx;
|
|
return;
|
|
}
|
|
}
|
|
|
|
int fallbackContinent = -1;
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
if (isLeafContinent(zones, i)) { fallbackContinent = i; break; }
|
|
}
|
|
if (fallbackContinent < 0) {
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
if (zones[i].areaID == 0 && !isRootContinent(zones, i)) {
|
|
fallbackContinent = i; break;
|
|
}
|
|
}
|
|
}
|
|
if (fallbackContinent >= 0) {
|
|
loadZoneTextures(fallbackContinent);
|
|
requestComposite(fallbackContinent);
|
|
currentIdx = fallbackContinent;
|
|
return;
|
|
}
|
|
|
|
currentIdx = -1;
|
|
compositedIdx = -1;
|
|
// Render target will be cleared by next compositePass
|
|
pendingCompositeIdx = -2; // Signal "clear only"
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Coordinate projection
|
|
// --------------------------------------------------------
|
|
|
|
glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) const {
|
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size()))
|
|
return glm::vec2(0.5f, 0.5f);
|
|
|
|
const auto& zone = zones[zoneIdx];
|
|
float wowX = renderPos.y;
|
|
float wowY = renderPos.x;
|
|
|
|
float left = zone.locLeft, right = zone.locRight;
|
|
float top = zone.locTop, bottom = zone.locBottom;
|
|
if (zone.areaID == 0) {
|
|
float l, r, t, b;
|
|
if (getContinentProjectionBounds(zoneIdx, l, r, t, b)) {
|
|
left = l; right = r; top = t; bottom = b;
|
|
}
|
|
}
|
|
|
|
float denom_h = left - right;
|
|
float denom_v = top - bottom;
|
|
if (std::abs(denom_h) < 0.001f || std::abs(denom_v) < 0.001f)
|
|
return glm::vec2(0.5f, 0.5f);
|
|
|
|
float u = (left - wowX) / denom_h;
|
|
float v = (top - wowY) / denom_v;
|
|
|
|
if (zone.areaID == 0) {
|
|
constexpr float kVScale = 1.0f;
|
|
constexpr float kVOffset = -0.15f;
|
|
v = (v - 0.5f) * kVScale + 0.5f + kVOffset;
|
|
}
|
|
return glm::vec2(u, v);
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Exploration tracking (identical to GL version)
|
|
// --------------------------------------------------------
|
|
|
|
void WorldMap::updateExploration(const glm::vec3& playerRenderPos) {
|
|
auto isExploreFlagSet = [this](uint32_t flag) -> bool {
|
|
if (!hasServerExplorationMask || serverExplorationMask.empty() || flag == 0) return false;
|
|
const auto isSet = [this](uint32_t bitIndex) -> bool {
|
|
const size_t word = bitIndex / 32;
|
|
if (word >= serverExplorationMask.size()) return false;
|
|
return (serverExplorationMask[word] & (1u << (bitIndex % 32))) != 0;
|
|
};
|
|
if (isSet(flag)) return true;
|
|
if (flag > 0 && isSet(flag - 1)) return true;
|
|
return false;
|
|
};
|
|
|
|
bool markedAny = false;
|
|
if (hasServerExplorationMask) {
|
|
exploredZones.clear();
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
const auto& z = zones[i];
|
|
if (z.areaID == 0 || z.exploreFlag == 0) continue;
|
|
if (isExploreFlagSet(z.exploreFlag)) {
|
|
exploredZones.insert(i);
|
|
markedAny = true;
|
|
}
|
|
}
|
|
}
|
|
if (markedAny) return;
|
|
|
|
float wowX = playerRenderPos.y;
|
|
float wowY = playerRenderPos.x;
|
|
|
|
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.locLeft, z.locRight), maxX = std::max(z.locLeft, z.locRight);
|
|
float minY = std::min(z.locTop, z.locBottom), maxY = std::max(z.locTop, z.locBottom);
|
|
if (maxX - minX < 0.001f || maxY - minY < 0.001f) continue;
|
|
if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) {
|
|
exploredZones.insert(i);
|
|
markedAny = true;
|
|
}
|
|
}
|
|
|
|
if (!markedAny) {
|
|
int zoneIdx = findZoneForPlayer(playerRenderPos);
|
|
if (zoneIdx >= 0) exploredZones.insert(zoneIdx);
|
|
}
|
|
}
|
|
|
|
void WorldMap::zoomIn(const glm::vec3& playerRenderPos) {
|
|
if (viewLevel == ViewLevel::WORLD) {
|
|
if (continentIdx >= 0) {
|
|
loadZoneTextures(continentIdx);
|
|
requestComposite(continentIdx);
|
|
currentIdx = continentIdx;
|
|
viewLevel = ViewLevel::CONTINENT;
|
|
}
|
|
} else if (viewLevel == ViewLevel::CONTINENT) {
|
|
int zoneIdx = findZoneForPlayer(playerRenderPos);
|
|
if (zoneIdx >= 0 && zoneBelongsToContinent(zoneIdx, continentIdx)) {
|
|
loadZoneTextures(zoneIdx);
|
|
requestComposite(zoneIdx);
|
|
currentIdx = zoneIdx;
|
|
viewLevel = ViewLevel::ZONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WorldMap::zoomOut() {
|
|
if (viewLevel == ViewLevel::ZONE) {
|
|
if (continentIdx >= 0) {
|
|
requestComposite(continentIdx);
|
|
currentIdx = continentIdx;
|
|
viewLevel = ViewLevel::CONTINENT;
|
|
}
|
|
} else if (viewLevel == ViewLevel::CONTINENT) {
|
|
enterWorldView();
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Main render (input + ImGui overlay)
|
|
// --------------------------------------------------------
|
|
|
|
void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) {
|
|
if (!initialized || !assetManager) return;
|
|
|
|
auto& input = core::Input::getInstance();
|
|
|
|
if (!zones.empty()) updateExploration(playerRenderPos);
|
|
|
|
if (open) {
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_M) ||
|
|
input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
|
|
open = false;
|
|
return;
|
|
}
|
|
|
|
auto& io = ImGui::GetIO();
|
|
float wheelDelta = io.MouseWheel;
|
|
if (std::abs(wheelDelta) < 0.001f)
|
|
wheelDelta = input.getMouseWheelDelta();
|
|
if (wheelDelta > 0.0f) zoomIn(playerRenderPos);
|
|
else if (wheelDelta < 0.0f) zoomOut();
|
|
} else {
|
|
auto& io = ImGui::GetIO();
|
|
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_M)) {
|
|
open = true;
|
|
if (zones.empty()) loadZonesFromDBC();
|
|
|
|
int bestContinent = findBestContinentForPlayer(playerRenderPos);
|
|
if (bestContinent >= 0 && bestContinent != continentIdx) {
|
|
continentIdx = bestContinent;
|
|
compositedIdx = -1;
|
|
}
|
|
|
|
int playerZone = findZoneForPlayer(playerRenderPos);
|
|
if (playerZone >= 0 && continentIdx >= 0 &&
|
|
zoneBelongsToContinent(playerZone, continentIdx)) {
|
|
loadZoneTextures(playerZone);
|
|
requestComposite(playerZone);
|
|
currentIdx = playerZone;
|
|
viewLevel = ViewLevel::ZONE;
|
|
} else if (continentIdx >= 0) {
|
|
loadZoneTextures(continentIdx);
|
|
requestComposite(continentIdx);
|
|
currentIdx = continentIdx;
|
|
viewLevel = ViewLevel::CONTINENT;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!open) return;
|
|
renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight);
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// ImGui overlay
|
|
// --------------------------------------------------------
|
|
|
|
void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) {
|
|
float sw = static_cast<float>(screenWidth);
|
|
float sh = static_cast<float>(screenHeight);
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
ImGui::SetNextWindowSize(ImVec2(sw, sh));
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse |
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing;
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.75f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
|
|
if (ImGui::Begin("##WorldMap", nullptr, flags)) {
|
|
float mapAspect = static_cast<float>(FBO_W) / static_cast<float>(FBO_H);
|
|
float availW = sw * 0.85f;
|
|
float availH = sh * 0.85f;
|
|
float displayW, displayH;
|
|
if (availW / availH > mapAspect) {
|
|
displayH = availH;
|
|
displayW = availH * mapAspect;
|
|
} else {
|
|
displayW = availW;
|
|
displayH = availW / mapAspect;
|
|
}
|
|
|
|
float mapX = (sw - displayW) / 2.0f;
|
|
float mapY = (sh - displayH) / 2.0f;
|
|
|
|
ImGui::SetCursorPos(ImVec2(mapX, mapY));
|
|
// Display composite render target via ImGui (VkDescriptorSet as ImTextureID)
|
|
ImGui::Image(reinterpret_cast<ImTextureID>(imguiDisplaySet),
|
|
ImVec2(displayW, displayH), ImVec2(0, 0), ImVec2(1, 1));
|
|
|
|
ImVec2 imgMin = ImGui::GetItemRectMin();
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
std::vector<int> continentIndices;
|
|
bool hasLeafContinents = false;
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
if (isLeafContinent(zones, i)) { hasLeafContinents = true; break; }
|
|
}
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
if (zones[i].areaID != 0) continue;
|
|
if (hasLeafContinents) {
|
|
if (isLeafContinent(zones, i)) continentIndices.push_back(i);
|
|
} else if (!isRootContinent(zones, i)) {
|
|
continentIndices.push_back(i);
|
|
}
|
|
}
|
|
if (continentIndices.size() > 1) {
|
|
std::vector<int> filtered;
|
|
filtered.reserve(continentIndices.size());
|
|
for (int idx : continentIndices) {
|
|
if (zones[idx].areaName == mapName) continue;
|
|
filtered.push_back(idx);
|
|
}
|
|
if (!filtered.empty()) continentIndices = std::move(filtered);
|
|
}
|
|
if (continentIndices.empty()) {
|
|
for (int i = 0; i < static_cast<int>(zones.size()); i++) {
|
|
if (zones[i].areaID == 0) continentIndices.push_back(i);
|
|
}
|
|
}
|
|
|
|
// World-level continent selection UI
|
|
if (viewLevel == ViewLevel::WORLD && !continentIndices.empty()) {
|
|
ImVec2 titleSz = ImGui::CalcTextSize("World");
|
|
ImGui::SetCursorPos(ImVec2((sw - titleSz.x) * 0.5f, mapY + 8.0f));
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 0.95f), "World");
|
|
|
|
ImGui::SetCursorPos(ImVec2(mapX + 8.0f, mapY + 32.0f));
|
|
for (size_t i = 0; i < continentIndices.size(); i++) {
|
|
int ci = continentIndices[i];
|
|
if (i > 0) ImGui::SameLine();
|
|
const bool selected = (ci == continentIdx);
|
|
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f));
|
|
|
|
std::string rawName = zones[ci].areaName.empty() ? "Continent" : zones[ci].areaName;
|
|
if (rawName == "Azeroth") rawName = "Eastern Kingdoms";
|
|
std::string label = rawName + "##" + std::to_string(ci);
|
|
if (ImGui::Button(label.c_str())) {
|
|
continentIdx = ci;
|
|
loadZoneTextures(continentIdx);
|
|
requestComposite(continentIdx);
|
|
currentIdx = continentIdx;
|
|
viewLevel = ViewLevel::CONTINENT;
|
|
}
|
|
if (selected) ImGui::PopStyleColor();
|
|
}
|
|
} else if (viewLevel == ViewLevel::CONTINENT && continentIndices.size() > 1) {
|
|
ImGui::SetCursorPos(ImVec2(mapX + 8.0f, mapY + 8.0f));
|
|
for (size_t i = 0; i < continentIndices.size(); i++) {
|
|
int ci = continentIndices[i];
|
|
if (i > 0) ImGui::SameLine();
|
|
const bool selected = (ci == continentIdx);
|
|
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f));
|
|
|
|
std::string rawName = zones[ci].areaName.empty() ? "Continent" : zones[ci].areaName;
|
|
if (rawName == "Azeroth") rawName = "Eastern Kingdoms";
|
|
std::string label = rawName + "##" + std::to_string(ci);
|
|
if (ImGui::Button(label.c_str())) {
|
|
continentIdx = ci;
|
|
loadZoneTextures(continentIdx);
|
|
requestComposite(continentIdx);
|
|
currentIdx = continentIdx;
|
|
}
|
|
if (selected) ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
// Player marker
|
|
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
|
glm::vec2 playerUV = renderPosToMapUV(playerRenderPos, currentIdx);
|
|
if (playerUV.x >= 0.0f && playerUV.x <= 1.0f &&
|
|
playerUV.y >= 0.0f && playerUV.y <= 1.0f) {
|
|
float px = imgMin.x + playerUV.x * displayW;
|
|
float py = imgMin.y + playerUV.y * displayH;
|
|
drawList->AddCircleFilled(ImVec2(px, py), 6.0f, IM_COL32(255, 40, 40, 255));
|
|
drawList->AddCircle(ImVec2(px, py), 6.0f, IM_COL32(0, 0, 0, 200), 0, 2.0f);
|
|
}
|
|
}
|
|
|
|
// Continent view: clickable zone overlays
|
|
if (viewLevel == ViewLevel::CONTINENT && continentIdx >= 0) {
|
|
const auto& cont = zones[continentIdx];
|
|
float cLeft = cont.locLeft, cRight = cont.locRight;
|
|
float cTop = cont.locTop, cBottom = cont.locBottom;
|
|
getContinentProjectionBounds(continentIdx, cLeft, cRight, cTop, cBottom);
|
|
float cDenomU = cLeft - cRight;
|
|
float cDenomV = cTop - cBottom;
|
|
|
|
ImVec2 mousePos = ImGui::GetMousePos();
|
|
int hoveredZone = -1;
|
|
|
|
if (std::abs(cDenomU) > 0.001f && std::abs(cDenomV) > 0.001f) {
|
|
for (int zi = 0; zi < static_cast<int>(zones.size()); zi++) {
|
|
if (!zoneBelongsToContinent(zi, continentIdx)) continue;
|
|
const auto& z = zones[zi];
|
|
if (std::abs(z.locLeft - z.locRight) < 0.001f ||
|
|
std::abs(z.locTop - z.locBottom) < 0.001f) continue;
|
|
|
|
float zuMin = (cLeft - z.locLeft) / cDenomU;
|
|
float zuMax = (cLeft - z.locRight) / cDenomU;
|
|
float zvMin = (cTop - z.locTop) / cDenomV;
|
|
float zvMax = (cTop - z.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 = imgMin.x + zuMin * displayW;
|
|
float sy0 = imgMin.y + zvMin * displayH;
|
|
float sx1 = imgMin.x + zuMax * displayW;
|
|
float sy1 = imgMin.y + zvMax * displayH;
|
|
|
|
bool explored = exploredZones.count(zi) > 0;
|
|
bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 &&
|
|
mousePos.y >= sy0 && mousePos.y <= sy1);
|
|
|
|
if (!explored) {
|
|
drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
|
IM_COL32(0, 0, 0, 160));
|
|
}
|
|
if (hovered) {
|
|
hoveredZone = zi;
|
|
drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
|
IM_COL32(255, 255, 200, 40));
|
|
drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
|
IM_COL32(255, 215, 0, 180), 0.0f, 0, 2.0f);
|
|
} else if (explored) {
|
|
drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
|
IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hoveredZone >= 0) {
|
|
ImGui::SetTooltip("%s", zones[hoveredZone].areaName.c_str());
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
loadZoneTextures(hoveredZone);
|
|
requestComposite(hoveredZone);
|
|
currentIdx = hoveredZone;
|
|
viewLevel = ViewLevel::ZONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Zone view: back to continent
|
|
if (viewLevel == ViewLevel::ZONE && continentIdx >= 0) {
|
|
auto& io = ImGui::GetIO();
|
|
bool goBack = io.MouseClicked[1];
|
|
|
|
ImGui::SetCursorPos(ImVec2(mapX + 8.0f, mapY + 8.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.1f, 0.9f));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
|
if (ImGui::Button("< Back")) goBack = true;
|
|
ImGui::PopStyleColor(3);
|
|
|
|
if (goBack) {
|
|
requestComposite(continentIdx);
|
|
currentIdx = continentIdx;
|
|
viewLevel = ViewLevel::CONTINENT;
|
|
}
|
|
|
|
const char* zoneName = zones[currentIdx].areaName.c_str();
|
|
ImVec2 nameSize = ImGui::CalcTextSize(zoneName);
|
|
float nameY = mapY - nameSize.y - 8.0f;
|
|
if (nameY > 0.0f) {
|
|
ImGui::SetCursorPos(ImVec2((sw - nameSize.x) / 2.0f, nameY));
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 0.9f), "%s", zoneName);
|
|
}
|
|
}
|
|
|
|
// Continent view: back to world
|
|
if (viewLevel == ViewLevel::CONTINENT) {
|
|
auto& io = ImGui::GetIO();
|
|
bool goWorld = io.MouseClicked[1];
|
|
|
|
float worldBtnY = mapY + (continentIndices.size() > 1 ? 40.0f : 8.0f);
|
|
ImGui::SetCursorPos(ImVec2(mapX + 8.0f, worldBtnY));
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.1f, 0.9f));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
|
if (ImGui::Button("< World")) goWorld = true;
|
|
ImGui::PopStyleColor(3);
|
|
|
|
if (goWorld) enterWorldView();
|
|
}
|
|
|
|
// Help text
|
|
const char* helpText;
|
|
if (viewLevel == ViewLevel::ZONE)
|
|
helpText = "Scroll out or right-click to zoom out | M or Escape to close";
|
|
else if (viewLevel == ViewLevel::WORLD)
|
|
helpText = "Select a continent | Scroll in to zoom | M or Escape to close";
|
|
else
|
|
helpText = "Click zone or scroll in to zoom | Scroll out / right-click for World | M or Escape to close";
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(helpText);
|
|
float textY = mapY + displayH + 8.0f;
|
|
if (textY + textSize.y < sh) {
|
|
ImGui::SetCursorPos(ImVec2((sw - textSize.x) / 2.0f, textY));
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.8f), "%s", helpText);
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
} // namespace rendering
|
|
} // namespace wowee
|