mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-02 15:53:51 +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
|
|
@ -263,6 +263,92 @@ endif()
|
|||
add_test(NAME transport_components COMMAND test_transport_components)
|
||||
register_test_target(test_transport_components)
|
||||
|
||||
# ── test_world_map ────────────────────────────────────────────
|
||||
add_executable(test_world_map
|
||||
test_world_map.cpp
|
||||
)
|
||||
target_include_directories(test_world_map PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_world_map SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_world_map PRIVATE catch2_main)
|
||||
if(TARGET glm::glm)
|
||||
target_link_libraries(test_world_map PRIVATE glm::glm)
|
||||
endif()
|
||||
add_test(NAME world_map COMMAND test_world_map)
|
||||
register_test_target(test_world_map)
|
||||
|
||||
# ── test_world_map_coordinate_projection ──────────────────────
|
||||
add_executable(test_world_map_coordinate_projection
|
||||
test_world_map_coordinate_projection.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/world_map/coordinate_projection.cpp
|
||||
)
|
||||
target_include_directories(test_world_map_coordinate_projection PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_world_map_coordinate_projection SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_world_map_coordinate_projection PRIVATE catch2_main)
|
||||
if(TARGET glm::glm)
|
||||
target_link_libraries(test_world_map_coordinate_projection PRIVATE glm::glm)
|
||||
endif()
|
||||
add_test(NAME world_map_coordinate_projection COMMAND test_world_map_coordinate_projection)
|
||||
register_test_target(test_world_map_coordinate_projection)
|
||||
|
||||
# ── test_world_map_map_resolver ───────────────────────────────
|
||||
add_executable(test_world_map_map_resolver
|
||||
test_world_map_map_resolver.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/world_map/map_resolver.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/world_map/coordinate_projection.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
)
|
||||
target_include_directories(test_world_map_map_resolver PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_world_map_map_resolver SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_world_map_map_resolver PRIVATE catch2_main)
|
||||
if(TARGET glm::glm)
|
||||
target_link_libraries(test_world_map_map_resolver PRIVATE glm::glm)
|
||||
endif()
|
||||
add_test(NAME world_map_map_resolver COMMAND test_world_map_map_resolver)
|
||||
register_test_target(test_world_map_map_resolver)
|
||||
|
||||
# ── test_world_map_view_state_machine ─────────────────────────
|
||||
add_executable(test_world_map_view_state_machine
|
||||
test_world_map_view_state_machine.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/world_map/view_state_machine.cpp
|
||||
)
|
||||
target_include_directories(test_world_map_view_state_machine PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_world_map_view_state_machine SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_world_map_view_state_machine PRIVATE catch2_main)
|
||||
if(TARGET glm::glm)
|
||||
target_link_libraries(test_world_map_view_state_machine PRIVATE glm::glm)
|
||||
endif()
|
||||
add_test(NAME world_map_view_state_machine COMMAND test_world_map_view_state_machine)
|
||||
register_test_target(test_world_map_view_state_machine)
|
||||
|
||||
# ── test_world_map_exploration_state ──────────────────────────
|
||||
add_executable(test_world_map_exploration_state
|
||||
test_world_map_exploration_state.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/world_map/exploration_state.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/world_map/coordinate_projection.cpp
|
||||
)
|
||||
target_include_directories(test_world_map_exploration_state PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_world_map_exploration_state SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_world_map_exploration_state PRIVATE catch2_main)
|
||||
if(TARGET glm::glm)
|
||||
target_link_libraries(test_world_map_exploration_state PRIVATE glm::glm)
|
||||
endif()
|
||||
add_test(NAME world_map_exploration_state COMMAND test_world_map_exploration_state)
|
||||
register_test_target(test_world_map_exploration_state)
|
||||
|
||||
# ── test_world_map_zone_metadata ──────────────────────────────
|
||||
add_executable(test_world_map_zone_metadata
|
||||
test_world_map_zone_metadata.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/world_map/zone_metadata.cpp
|
||||
)
|
||||
target_include_directories(test_world_map_zone_metadata PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_world_map_zone_metadata SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_world_map_zone_metadata PRIVATE catch2_main)
|
||||
if(TARGET glm::glm)
|
||||
target_link_libraries(test_world_map_zone_metadata PRIVATE glm::glm)
|
||||
endif()
|
||||
add_test(NAME world_map_zone_metadata COMMAND test_world_map_zone_metadata)
|
||||
register_test_target(test_world_map_zone_metadata)
|
||||
|
||||
# ── ASAN / UBSan for test targets ────────────────────────────
|
||||
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
|
||||
foreach(_t IN LISTS ALL_TEST_TARGETS)
|
||||
|
|
|
|||
498
tests/test_world_map.cpp
Normal file
498
tests/test_world_map.cpp
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
// Tests for WorldMap data structures and coordinate math
|
||||
// Updated to use new modular types from world_map_types.hpp
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/world_map/world_map_types.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
using wowee::rendering::world_map::Zone;
|
||||
using wowee::rendering::world_map::OverlayEntry;
|
||||
using wowee::rendering::world_map::POI;
|
||||
|
||||
// ── MapPOI struct ────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("POI default-constructed is zeroed", "[world_map]") {
|
||||
POI poi{};
|
||||
REQUIRE(poi.id == 0);
|
||||
REQUIRE(poi.importance == 0);
|
||||
REQUIRE(poi.iconType == 0);
|
||||
REQUIRE(poi.factionId == 0);
|
||||
REQUIRE(poi.wowX == 0.0f);
|
||||
REQUIRE(poi.wowY == 0.0f);
|
||||
REQUIRE(poi.wowZ == 0.0f);
|
||||
REQUIRE(poi.mapId == 0);
|
||||
REQUIRE(poi.name.empty());
|
||||
REQUIRE(poi.description.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("POI sorts by importance ascending", "[world_map]") {
|
||||
std::vector<POI> pois;
|
||||
|
||||
POI capital;
|
||||
capital.id = 1;
|
||||
capital.importance = 3;
|
||||
capital.name = "Stormwind";
|
||||
pois.push_back(capital);
|
||||
|
||||
POI town;
|
||||
town.id = 2;
|
||||
town.importance = 1;
|
||||
town.name = "Goldshire";
|
||||
pois.push_back(town);
|
||||
|
||||
POI minor;
|
||||
minor.id = 3;
|
||||
minor.importance = 0;
|
||||
minor.name = "Mirror Lake";
|
||||
pois.push_back(minor);
|
||||
|
||||
std::sort(pois.begin(), pois.end(), [](const POI& a, const POI& b) {
|
||||
return a.importance < b.importance;
|
||||
});
|
||||
|
||||
REQUIRE(pois[0].name == "Mirror Lake");
|
||||
REQUIRE(pois[1].name == "Goldshire");
|
||||
REQUIRE(pois[2].name == "Stormwind");
|
||||
}
|
||||
|
||||
// ── WorldMapZone struct ──────────────────────────────────────
|
||||
|
||||
TEST_CASE("Zone default-constructed is valid", "[world_map]") {
|
||||
Zone z{};
|
||||
REQUIRE(z.wmaID == 0);
|
||||
REQUIRE(z.areaID == 0);
|
||||
REQUIRE(z.areaName.empty());
|
||||
REQUIRE(z.bounds.locLeft == 0.0f);
|
||||
REQUIRE(z.bounds.locRight == 0.0f);
|
||||
REQUIRE(z.bounds.locTop == 0.0f);
|
||||
REQUIRE(z.bounds.locBottom == 0.0f);
|
||||
REQUIRE(z.displayMapID == 0);
|
||||
REQUIRE(z.parentWorldMapID == 0);
|
||||
REQUIRE(z.exploreBits.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Zone areaID==0 identifies continent", "[world_map]") {
|
||||
Zone continent{};
|
||||
continent.areaID = 0;
|
||||
continent.wmaID = 10;
|
||||
continent.areaName = "Kalimdor";
|
||||
|
||||
Zone zone{};
|
||||
zone.areaID = 440;
|
||||
zone.wmaID = 100;
|
||||
zone.areaName = "Tanaris";
|
||||
|
||||
REQUIRE(continent.areaID == 0);
|
||||
REQUIRE(zone.areaID != 0);
|
||||
}
|
||||
|
||||
// ── Coordinate projection logic ──────────────────────────────
|
||||
// Replicate the UV projection formula from renderPosToMapUV for standalone testing.
|
||||
|
||||
static glm::vec2 computeMapUV(float wowX, float wowY,
|
||||
float locLeft, float locRight,
|
||||
float locTop, float locBottom,
|
||||
bool isContinent) {
|
||||
float denom_h = locLeft - locRight;
|
||||
float denom_v = locTop - locBottom;
|
||||
if (std::abs(denom_h) < 0.001f || std::abs(denom_v) < 0.001f)
|
||||
return glm::vec2(0.5f, 0.5f);
|
||||
|
||||
float u = (locLeft - wowX) / denom_h;
|
||||
float v = (locTop - wowY) / denom_v;
|
||||
|
||||
if (isContinent) {
|
||||
constexpr float kVOffset = -0.15f;
|
||||
v = (v - 0.5f) + 0.5f + kVOffset;
|
||||
}
|
||||
return glm::vec2(u, v);
|
||||
}
|
||||
|
||||
TEST_CASE("UV projection: center of zone maps to (0.5, 0.5)", "[world_map]") {
|
||||
// Zone bounds: left=1000, right=0, top=1000, bottom=0
|
||||
float centerX = 500.0f, centerY = 500.0f;
|
||||
glm::vec2 uv = computeMapUV(centerX, centerY, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||
REQUIRE(uv.x == Catch::Approx(0.5f).margin(0.001f));
|
||||
REQUIRE(uv.y == Catch::Approx(0.5f).margin(0.001f));
|
||||
}
|
||||
|
||||
TEST_CASE("UV projection: top-left corner maps to (0, 0)", "[world_map]") {
|
||||
glm::vec2 uv = computeMapUV(1000.0f, 1000.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||
REQUIRE(uv.x == Catch::Approx(0.0f).margin(0.001f));
|
||||
REQUIRE(uv.y == Catch::Approx(0.0f).margin(0.001f));
|
||||
}
|
||||
|
||||
TEST_CASE("UV projection: bottom-right corner maps to (1, 1)", "[world_map]") {
|
||||
glm::vec2 uv = computeMapUV(0.0f, 0.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||
REQUIRE(uv.x == Catch::Approx(1.0f).margin(0.001f));
|
||||
REQUIRE(uv.y == Catch::Approx(1.0f).margin(0.001f));
|
||||
}
|
||||
|
||||
TEST_CASE("UV projection: degenerate bounds returns center", "[world_map]") {
|
||||
// left == right → degenerate
|
||||
glm::vec2 uv = computeMapUV(500.0f, 500.0f, 500.0f, 500.0f, 1000.0f, 0.0f, false);
|
||||
REQUIRE(uv.x == Catch::Approx(0.5f));
|
||||
REQUIRE(uv.y == Catch::Approx(0.5f));
|
||||
}
|
||||
|
||||
TEST_CASE("UV projection: continent mode applies vertical offset", "[world_map]") {
|
||||
// Same center point, but continent mode shifts V by kVOffset=-0.15
|
||||
glm::vec2 uvZone = computeMapUV(500.0f, 500.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, false);
|
||||
glm::vec2 uvCont = computeMapUV(500.0f, 500.0f, 1000.0f, 0.0f, 1000.0f, 0.0f, true);
|
||||
|
||||
REQUIRE(uvZone.x == Catch::Approx(uvCont.x).margin(0.001f));
|
||||
// Continent V should be shifted by -0.15
|
||||
REQUIRE(uvCont.y == Catch::Approx(uvZone.y - 0.15f).margin(0.001f));
|
||||
}
|
||||
|
||||
// ── Expansion level derivation ───────────────────────────────
|
||||
// Replicate the expansion detection logic from getExpansionLevel.
|
||||
|
||||
static int deriveExpansionLevel(int maxLevel) {
|
||||
if (maxLevel <= 60) return 0; // vanilla
|
||||
if (maxLevel <= 70) return 1; // TBC
|
||||
return 2; // WotLK
|
||||
}
|
||||
|
||||
TEST_CASE("Expansion level from maxLevel", "[world_map]") {
|
||||
REQUIRE(deriveExpansionLevel(60) == 0); // vanilla
|
||||
REQUIRE(deriveExpansionLevel(58) == 0); // below vanilla cap
|
||||
REQUIRE(deriveExpansionLevel(70) == 1); // TBC
|
||||
REQUIRE(deriveExpansionLevel(65) == 1); // mid TBC range
|
||||
REQUIRE(deriveExpansionLevel(80) == 2); // WotLK
|
||||
REQUIRE(deriveExpansionLevel(75) == 2); // mid WotLK range
|
||||
}
|
||||
|
||||
// ── Expansion continent filtering ────────────────────────────
|
||||
|
||||
static std::vector<uint32_t> filterContinentsByExpansion(
|
||||
const std::vector<uint32_t>& mapIds, int expansionLevel) {
|
||||
std::vector<uint32_t> result;
|
||||
for (uint32_t id : mapIds) {
|
||||
if (id == 530 && expansionLevel < 1) continue;
|
||||
if (id == 571 && expansionLevel < 2) continue;
|
||||
result.push_back(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST_CASE("Vanilla hides TBC and WotLK continents", "[world_map]") {
|
||||
std::vector<uint32_t> all = {0, 1, 530, 571};
|
||||
auto filtered = filterContinentsByExpansion(all, 0);
|
||||
REQUIRE(filtered.size() == 2);
|
||||
REQUIRE(filtered[0] == 0);
|
||||
REQUIRE(filtered[1] == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("TBC shows Outland but hides Northrend", "[world_map]") {
|
||||
std::vector<uint32_t> all = {0, 1, 530, 571};
|
||||
auto filtered = filterContinentsByExpansion(all, 1);
|
||||
REQUIRE(filtered.size() == 3);
|
||||
REQUIRE(filtered[2] == 530);
|
||||
}
|
||||
|
||||
TEST_CASE("WotLK shows all continents", "[world_map]") {
|
||||
std::vector<uint32_t> all = {0, 1, 530, 571};
|
||||
auto filtered = filterContinentsByExpansion(all, 2);
|
||||
REQUIRE(filtered.size() == 4);
|
||||
}
|
||||
|
||||
// ── POI faction coloring logic ───────────────────────────────
|
||||
|
||||
enum class Faction { Alliance, Horde, Neutral };
|
||||
|
||||
static Faction classifyFaction(uint32_t factionId) {
|
||||
if (factionId == 469) return Faction::Alliance;
|
||||
if (factionId == 67) return Faction::Horde;
|
||||
return Faction::Neutral;
|
||||
}
|
||||
|
||||
TEST_CASE("POI faction classification", "[world_map]") {
|
||||
REQUIRE(classifyFaction(469) == Faction::Alliance);
|
||||
REQUIRE(classifyFaction(67) == Faction::Horde);
|
||||
REQUIRE(classifyFaction(0) == Faction::Neutral);
|
||||
REQUIRE(classifyFaction(35) == Faction::Neutral);
|
||||
}
|
||||
|
||||
// ── Overlay entry defaults ───────────────────────────────────
|
||||
|
||||
TEST_CASE("OverlayEntry defaults", "[world_map]") {
|
||||
OverlayEntry ov{};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
REQUIRE(ov.areaIDs[i] == 0);
|
||||
}
|
||||
REQUIRE(ov.textureName.empty());
|
||||
REQUIRE(ov.texWidth == 0);
|
||||
REQUIRE(ov.texHeight == 0);
|
||||
REQUIRE(ov.offsetX == 0);
|
||||
REQUIRE(ov.offsetY == 0);
|
||||
REQUIRE(ov.hitRectLeft == 0);
|
||||
REQUIRE(ov.hitRectRight == 0);
|
||||
REQUIRE(ov.hitRectTop == 0);
|
||||
REQUIRE(ov.hitRectBottom == 0);
|
||||
REQUIRE(ov.tileCols == 0);
|
||||
REQUIRE(ov.tileRows == 0);
|
||||
REQUIRE(ov.tilesLoaded == false);
|
||||
}
|
||||
|
||||
// ── ZMP pixel-map zone lookup ────────────────────────────────
|
||||
|
||||
TEST_CASE("ZMP grid lookup resolves mouse UV to zone", "[world_map]") {
|
||||
// Simulate a 128x128 ZMP grid with a zone at a known cell
|
||||
std::array<uint32_t, 128 * 128> grid{};
|
||||
uint32_t testAreaId = 42;
|
||||
// Place area ID at grid cell (64, 64) — center of map
|
||||
grid[64 * 128 + 64] = testAreaId;
|
||||
|
||||
// Mouse at UV (0.5, 0.5) → col=64, row=64
|
||||
float mu = 0.5f, mv = 0.5f;
|
||||
constexpr int ZMP_SIZE = 128;
|
||||
int col = std::clamp(static_cast<int>(mu * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||
int row = std::clamp(static_cast<int>(mv * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||
uint32_t areaId = grid[row * ZMP_SIZE + col];
|
||||
REQUIRE(areaId == testAreaId);
|
||||
}
|
||||
|
||||
TEST_CASE("ZMP grid returns 0 for empty cells", "[world_map]") {
|
||||
std::array<uint32_t, 128 * 128> grid{};
|
||||
// Empty grid — all cells zero (ocean/no zone)
|
||||
constexpr int ZMP_SIZE = 128;
|
||||
int col = 10, row = 10;
|
||||
REQUIRE(grid[row * ZMP_SIZE + col] == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("ZMP grid clamps out-of-range UV", "[world_map]") {
|
||||
std::array<uint32_t, 128 * 128> grid{};
|
||||
grid[0] = 100; // (0,0) cell
|
||||
grid[127 * 128 + 127] = 200; // (127,127) cell
|
||||
|
||||
constexpr int ZMP_SIZE = 128;
|
||||
// UV at (-0.1, -0.1) should clamp to (0, 0)
|
||||
float mu = -0.1f, mv = -0.1f;
|
||||
int col = std::clamp(static_cast<int>(mu * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||
int row = std::clamp(static_cast<int>(mv * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||
REQUIRE(grid[row * ZMP_SIZE + col] == 100);
|
||||
|
||||
// UV at (1.5, 1.5) should clamp to (127, 127)
|
||||
mu = 1.5f; mv = 1.5f;
|
||||
col = std::clamp(static_cast<int>(mu * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||
row = std::clamp(static_cast<int>(mv * ZMP_SIZE), 0, ZMP_SIZE - 1);
|
||||
REQUIRE(grid[row * ZMP_SIZE + col] == 200);
|
||||
}
|
||||
|
||||
// ── HitRect overlay AABB pre-filter ──────────────────────────
|
||||
|
||||
TEST_CASE("HitRect filters overlays correctly", "[world_map]") {
|
||||
OverlayEntry ov{};
|
||||
ov.hitRectLeft = 100;
|
||||
ov.hitRectRight = 300;
|
||||
ov.hitRectTop = 50;
|
||||
ov.hitRectBottom = 200;
|
||||
ov.texWidth = 200;
|
||||
ov.texHeight = 150;
|
||||
ov.textureName = "Goldshire";
|
||||
|
||||
bool hasHitRect = (ov.hitRectRight > ov.hitRectLeft &&
|
||||
ov.hitRectBottom > ov.hitRectTop);
|
||||
REQUIRE(hasHitRect);
|
||||
|
||||
// Point inside HitRect
|
||||
float px = 150.0f, py = 100.0f;
|
||||
bool inside = (px >= ov.hitRectLeft && px <= ov.hitRectRight &&
|
||||
py >= ov.hitRectTop && py <= ov.hitRectBottom);
|
||||
REQUIRE(inside);
|
||||
|
||||
// Point outside HitRect
|
||||
px = 50.0f; py = 25.0f;
|
||||
inside = (px >= ov.hitRectLeft && px <= ov.hitRectRight &&
|
||||
py >= ov.hitRectTop && py <= ov.hitRectBottom);
|
||||
REQUIRE_FALSE(inside);
|
||||
}
|
||||
|
||||
TEST_CASE("HitRect with zero values falls back to offset AABB", "[world_map]") {
|
||||
OverlayEntry ov{};
|
||||
// HitRect fields all zero → hasHitRect should be false
|
||||
bool hasHitRect = (ov.hitRectRight > ov.hitRectLeft &&
|
||||
ov.hitRectBottom > ov.hitRectTop);
|
||||
REQUIRE_FALSE(hasHitRect);
|
||||
}
|
||||
|
||||
TEST_CASE("Subzone hover with HitRect picks smallest overlay", "[world_map]") {
|
||||
// Simulate two overlays — one large with HitRect, one small with HitRect
|
||||
struct TestHitOverlay {
|
||||
float hitLeft, hitRight, hitTop, hitBottom;
|
||||
float texW, texH;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
TestHitOverlay large{0.0f, 500.0f, 0.0f, 400.0f, 500.0f, 400.0f, "BigArea"};
|
||||
TestHitOverlay small{100.0f, 250.0f, 80.0f, 180.0f, 150.0f, 100.0f, "SmallArea"};
|
||||
std::vector<TestHitOverlay> overlays = {large, small};
|
||||
|
||||
float px = 150.0f, py = 120.0f; // Inside both HitRects
|
||||
std::string best;
|
||||
float bestArea = std::numeric_limits<float>::max();
|
||||
for (const auto& ov : overlays) {
|
||||
bool inside = (px >= ov.hitLeft && px <= ov.hitRight &&
|
||||
py >= ov.hitTop && py <= ov.hitBottom);
|
||||
if (inside) {
|
||||
float area = ov.texW * ov.texH;
|
||||
if (area < bestArea) {
|
||||
bestArea = area;
|
||||
best = ov.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUIRE(best == "SmallArea");
|
||||
}
|
||||
|
||||
// ── Cosmic view expansion logic ──────────────────────────────
|
||||
|
||||
struct CosmicMapEntry {
|
||||
int mapId = 0;
|
||||
std::string label;
|
||||
};
|
||||
|
||||
static std::vector<CosmicMapEntry> buildCosmicMaps(int expLevel) {
|
||||
std::vector<CosmicMapEntry> maps;
|
||||
if (expLevel == 0) return maps; // Vanilla: no cosmic
|
||||
maps.push_back({0, "Azeroth"});
|
||||
if (expLevel >= 1) maps.push_back({530, "Outland"});
|
||||
if (expLevel >= 2) maps.push_back({571, "Northrend"});
|
||||
return maps;
|
||||
}
|
||||
|
||||
TEST_CASE("Vanilla has no cosmic view entries", "[world_map]") {
|
||||
auto maps = buildCosmicMaps(0);
|
||||
REQUIRE(maps.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("TBC cosmic view has Azeroth + Outland", "[world_map]") {
|
||||
auto maps = buildCosmicMaps(1);
|
||||
REQUIRE(maps.size() == 2);
|
||||
REQUIRE(maps[0].mapId == 0);
|
||||
REQUIRE(maps[0].label == "Azeroth");
|
||||
REQUIRE(maps[1].mapId == 530);
|
||||
REQUIRE(maps[1].label == "Outland");
|
||||
}
|
||||
|
||||
TEST_CASE("WotLK cosmic view has all three worlds", "[world_map]") {
|
||||
auto maps = buildCosmicMaps(2);
|
||||
REQUIRE(maps.size() == 3);
|
||||
REQUIRE(maps[2].mapId == 571);
|
||||
REQUIRE(maps[2].label == "Northrend");
|
||||
}
|
||||
|
||||
// ── Subzone hover priority (smallest overlay wins) ───────────
|
||||
|
||||
struct TestOverlay {
|
||||
float offsetX, offsetY;
|
||||
float width, height;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
static std::string findSmallestOverlay(const std::vector<TestOverlay>& overlays,
|
||||
float mu, float mv, float fboW, float fboH) {
|
||||
std::string best;
|
||||
float bestArea = std::numeric_limits<float>::max();
|
||||
for (const auto& ov : overlays) {
|
||||
float ovLeft = ov.offsetX / fboW;
|
||||
float ovTop = ov.offsetY / fboH;
|
||||
float ovRight = (ov.offsetX + ov.width) / fboW;
|
||||
float ovBottom = (ov.offsetY + ov.height) / fboH;
|
||||
if (mu >= ovLeft && mu <= ovRight && mv >= ovTop && mv <= ovBottom) {
|
||||
float area = ov.width * ov.height;
|
||||
if (area < bestArea) {
|
||||
bestArea = area;
|
||||
best = ov.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
TEST_CASE("Subzone hover returns smallest overlapping overlay", "[world_map]") {
|
||||
// Large overlay covers 0-512, 0-512
|
||||
TestOverlay large{0.0f, 0.0f, 512.0f, 512.0f, "BigZone"};
|
||||
// Small overlay covers 100-200, 100-200
|
||||
TestOverlay small{100.0f, 100.0f, 100.0f, 100.0f, "SmallSubzone"};
|
||||
|
||||
std::vector<TestOverlay> overlays = {large, small};
|
||||
|
||||
// Mouse at UV (0.15, 0.15) → pixel (153.6, 115.2) for 1024x768 FBO
|
||||
// Both overlays overlap; small should win
|
||||
std::string result = findSmallestOverlay(overlays, 0.15f, 0.15f, 1024.0f, 768.0f);
|
||||
REQUIRE(result == "SmallSubzone");
|
||||
}
|
||||
|
||||
TEST_CASE("Subzone hover returns only overlay when one matches", "[world_map]") {
|
||||
TestOverlay large{0.0f, 0.0f, 512.0f, 512.0f, "BigZone"};
|
||||
TestOverlay small{100.0f, 100.0f, 100.0f, 100.0f, "SmallSubzone"};
|
||||
std::vector<TestOverlay> overlays = {large, small};
|
||||
|
||||
// Mouse at UV (0.01, 0.01) → only large matches
|
||||
std::string result = findSmallestOverlay(overlays, 0.01f, 0.01f, 1024.0f, 768.0f);
|
||||
REQUIRE(result == "BigZone");
|
||||
}
|
||||
|
||||
TEST_CASE("Subzone hover returns empty when nothing matches", "[world_map]") {
|
||||
TestOverlay ov{100.0f, 100.0f, 50.0f, 50.0f, "Tiny"};
|
||||
std::vector<TestOverlay> overlays = {ov};
|
||||
|
||||
std::string result = findSmallestOverlay(overlays, 0.01f, 0.01f, 1024.0f, 768.0f);
|
||||
REQUIRE(result.empty());
|
||||
}
|
||||
|
||||
// ── Zone metadata (level range + faction) ────────────────────
|
||||
|
||||
enum class TestFaction { Neutral, Alliance, Horde, Contested };
|
||||
|
||||
struct TestZoneMeta {
|
||||
uint8_t minLevel = 0, maxLevel = 0;
|
||||
TestFaction faction = TestFaction::Neutral;
|
||||
};
|
||||
|
||||
static std::string formatZoneLabel(const std::string& name, const TestZoneMeta* meta) {
|
||||
std::string label = name;
|
||||
if (meta) {
|
||||
if (meta->minLevel > 0 && meta->maxLevel > 0) {
|
||||
label += " (" + std::to_string(meta->minLevel) + "-" +
|
||||
std::to_string(meta->maxLevel) + ")";
|
||||
}
|
||||
switch (meta->faction) {
|
||||
case TestFaction::Alliance: label += " [Alliance]"; break;
|
||||
case TestFaction::Horde: label += " [Horde]"; break;
|
||||
case TestFaction::Contested: label += " [Contested]"; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
TEST_CASE("Zone label includes level range and faction", "[world_map]") {
|
||||
TestZoneMeta meta{1, 10, TestFaction::Alliance};
|
||||
std::string label = formatZoneLabel("Elwynn", &meta);
|
||||
REQUIRE(label == "Elwynn (1-10) [Alliance]");
|
||||
}
|
||||
|
||||
TEST_CASE("Zone label shows Contested faction", "[world_map]") {
|
||||
TestZoneMeta meta{30, 45, TestFaction::Contested};
|
||||
std::string label = formatZoneLabel("Stranglethorn", &meta);
|
||||
REQUIRE(label == "Stranglethorn (30-45) [Contested]");
|
||||
}
|
||||
|
||||
TEST_CASE("Zone label without metadata is just the name", "[world_map]") {
|
||||
std::string label = formatZoneLabel("UnknownZone", nullptr);
|
||||
REQUIRE(label == "UnknownZone");
|
||||
}
|
||||
|
||||
TEST_CASE("Zone label with Neutral faction omits tag", "[world_map]") {
|
||||
TestZoneMeta meta{55, 60, TestFaction::Neutral};
|
||||
std::string label = formatZoneLabel("Moonglade", &meta);
|
||||
REQUIRE(label == "Moonglade (55-60)");
|
||||
}
|
||||
193
tests/test_world_map_coordinate_projection.cpp
Normal file
193
tests/test_world_map_coordinate_projection.cpp
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Tests for the extracted world map coordinate projection module
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/world_map/coordinate_projection.hpp"
|
||||
#include "rendering/world_map/world_map_types.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
using namespace wowee::rendering::world_map;
|
||||
|
||||
// ── Helper: build a minimal zone for testing ─────────────────
|
||||
|
||||
static Zone makeZone(uint32_t wmaID, uint32_t areaID,
|
||||
float locLeft, float locRight,
|
||||
float locTop, float locBottom,
|
||||
uint32_t displayMapID = 0,
|
||||
uint32_t parentWorldMapID = 0,
|
||||
const std::string& name = "") {
|
||||
Zone z;
|
||||
z.wmaID = wmaID;
|
||||
z.areaID = areaID;
|
||||
z.areaName = name;
|
||||
z.bounds.locLeft = locLeft;
|
||||
z.bounds.locRight = locRight;
|
||||
z.bounds.locTop = locTop;
|
||||
z.bounds.locBottom = locBottom;
|
||||
z.displayMapID = displayMapID;
|
||||
z.parentWorldMapID = parentWorldMapID;
|
||||
return z;
|
||||
}
|
||||
|
||||
// ── renderPosToMapUV ─────────────────────────────────────────
|
||||
|
||||
TEST_CASE("renderPosToMapUV: center of zone maps to (0.5, ~0.5)", "[world_map][coordinate_projection]") {
|
||||
ZoneBounds bounds;
|
||||
bounds.locLeft = 1000.0f;
|
||||
bounds.locRight = -1000.0f;
|
||||
bounds.locTop = 1000.0f;
|
||||
bounds.locBottom = -1000.0f;
|
||||
|
||||
// renderPos.y = wowX, renderPos.x = wowY
|
||||
glm::vec3 center(0.0f, 0.0f, 0.0f);
|
||||
glm::vec2 uv = renderPosToMapUV(center, bounds, /*isContinent=*/false);
|
||||
REQUIRE(std::abs(uv.x - 0.5f) < 0.01f);
|
||||
REQUIRE(std::abs(uv.y - 0.5f) < 0.01f);
|
||||
}
|
||||
|
||||
TEST_CASE("renderPosToMapUV: degenerate bounds returns (0.5, 0.5)", "[world_map][coordinate_projection]") {
|
||||
ZoneBounds bounds{}; // all zeros
|
||||
glm::vec3 pos(100.0f, 200.0f, 0.0f);
|
||||
glm::vec2 uv = renderPosToMapUV(pos, bounds, false);
|
||||
REQUIRE(uv.x == Catch::Approx(0.5f));
|
||||
REQUIRE(uv.y == Catch::Approx(0.5f));
|
||||
}
|
||||
|
||||
TEST_CASE("renderPosToMapUV: top-left corner maps to (0, ~0)", "[world_map][coordinate_projection]") {
|
||||
ZoneBounds bounds;
|
||||
bounds.locLeft = 1000.0f;
|
||||
bounds.locRight = -1000.0f;
|
||||
bounds.locTop = 1000.0f;
|
||||
bounds.locBottom = -1000.0f;
|
||||
|
||||
// wowX = renderPos.y = locLeft = 1000, wowY = renderPos.x = locTop = 1000
|
||||
glm::vec3 topLeft(1000.0f, 1000.0f, 0.0f);
|
||||
glm::vec2 uv = renderPosToMapUV(topLeft, bounds, false);
|
||||
REQUIRE(uv.x == Catch::Approx(0.0f).margin(0.01f));
|
||||
REQUIRE(uv.y == Catch::Approx(0.0f).margin(0.01f));
|
||||
}
|
||||
|
||||
TEST_CASE("renderPosToMapUV: continent applies vertical offset", "[world_map][coordinate_projection]") {
|
||||
ZoneBounds bounds;
|
||||
bounds.locLeft = 1000.0f;
|
||||
bounds.locRight = -1000.0f;
|
||||
bounds.locTop = 1000.0f;
|
||||
bounds.locBottom = -1000.0f;
|
||||
|
||||
glm::vec3 center(0.0f, 0.0f, 0.0f);
|
||||
glm::vec2 zone_uv = renderPosToMapUV(center, bounds, false);
|
||||
glm::vec2 cont_uv = renderPosToMapUV(center, bounds, true);
|
||||
|
||||
// Continent mode applies kVOffset = -0.15
|
||||
REQUIRE(zone_uv.x == Catch::Approx(cont_uv.x).margin(0.01f));
|
||||
REQUIRE(cont_uv.y != Catch::Approx(zone_uv.y).margin(0.01f));
|
||||
}
|
||||
|
||||
// ── zoneBelongsToContinent ───────────────────────────────────
|
||||
|
||||
TEST_CASE("zoneBelongsToContinent: parent match", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
// Continent at index 0
|
||||
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "EK"));
|
||||
// Zone at index 1: parentWorldMapID matches continent's wmaID
|
||||
zones.push_back(makeZone(2, 100, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0, 1, "Elwynn"));
|
||||
|
||||
REQUIRE(zoneBelongsToContinent(zones, 1, 0) == true);
|
||||
}
|
||||
|
||||
TEST_CASE("zoneBelongsToContinent: no relation", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "EK"));
|
||||
zones.push_back(makeZone(99, 100, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0, 50, "Far"));
|
||||
|
||||
REQUIRE(zoneBelongsToContinent(zones, 1, 0) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("zoneBelongsToContinent: out of bounds returns false", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f));
|
||||
REQUIRE(zoneBelongsToContinent(zones, -1, 0) == false);
|
||||
REQUIRE(zoneBelongsToContinent(zones, 5, 0) == false);
|
||||
}
|
||||
|
||||
// ── isRootContinent / isLeafContinent ────────────────────────
|
||||
|
||||
TEST_CASE("isRootContinent detects root with leaf children", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "Root"));
|
||||
zones.push_back(makeZone(2, 0, 3000.0f, -3000.0f, 3000.0f, -3000.0f, 0, 1, "Leaf"));
|
||||
|
||||
REQUIRE(isRootContinent(zones, 0) == true);
|
||||
REQUIRE(isLeafContinent(zones, 1) == true);
|
||||
REQUIRE(isRootContinent(zones, 1) == false);
|
||||
REQUIRE(isLeafContinent(zones, 0) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("isRootContinent: lone continent is not root (no children)", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 0, "Solo"));
|
||||
REQUIRE(isRootContinent(zones, 0) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("isRootContinent: out of bounds returns false", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
REQUIRE(isRootContinent(zones, 0) == false);
|
||||
REQUIRE(isRootContinent(zones, -1) == false);
|
||||
REQUIRE(isLeafContinent(zones, 0) == false);
|
||||
}
|
||||
|
||||
// ── findZoneForPlayer ────────────────────────────────────────
|
||||
|
||||
TEST_CASE("findZoneForPlayer: finds smallest containing zone", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
// Continent (ignored: areaID == 0)
|
||||
zones.push_back(makeZone(1, 0, 10000.0f, -10000.0f, 10000.0f, -10000.0f, 0, 0, "Cont"));
|
||||
// Large zone
|
||||
zones.push_back(makeZone(2, 100, 5000.0f, -5000.0f, 5000.0f, -5000.0f, 0, 1, "Large"));
|
||||
// Small zone fully inside large
|
||||
zones.push_back(makeZone(3, 200, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0, 1, "Small"));
|
||||
|
||||
// Player at center — should find the smaller zone
|
||||
glm::vec3 playerPos(0.0f, 0.0f, 0.0f);
|
||||
int found = findZoneForPlayer(zones, playerPos);
|
||||
REQUIRE(found == 2); // Small zone
|
||||
}
|
||||
|
||||
TEST_CASE("findZoneForPlayer: returns -1 when no zone contains position", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 100, 100.0f, -100.0f, 100.0f, -100.0f, 0, 0, "Tiny"));
|
||||
|
||||
glm::vec3 farAway(9999.0f, 9999.0f, 0.0f);
|
||||
REQUIRE(findZoneForPlayer(zones, farAway) == -1);
|
||||
}
|
||||
|
||||
// ── getContinentProjectionBounds ─────────────────────────────
|
||||
|
||||
TEST_CASE("getContinentProjectionBounds: uses continent's own bounds if available", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 0, 5000.0f, -5000.0f, 3000.0f, -3000.0f, 0, 0, "EK"));
|
||||
|
||||
float l, r, t, b;
|
||||
bool ok = getContinentProjectionBounds(zones, 0, l, r, t, b);
|
||||
REQUIRE(ok == true);
|
||||
REQUIRE(l == Catch::Approx(5000.0f));
|
||||
REQUIRE(r == Catch::Approx(-5000.0f));
|
||||
REQUIRE(t == Catch::Approx(3000.0f));
|
||||
REQUIRE(b == Catch::Approx(-3000.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("getContinentProjectionBounds: returns false for out of bounds", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
float l, r, t, b;
|
||||
REQUIRE(getContinentProjectionBounds(zones, 0, l, r, t, b) == false);
|
||||
REQUIRE(getContinentProjectionBounds(zones, -1, l, r, t, b) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("getContinentProjectionBounds: rejects non-continent zones", "[world_map][coordinate_projection]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 100, 5000.0f, -5000.0f, 3000.0f, -3000.0f, 0, 0, "Zone"));
|
||||
float l, r, t, b;
|
||||
bool ok = getContinentProjectionBounds(zones, 0, l, r, t, b);
|
||||
REQUIRE(ok == false);
|
||||
}
|
||||
89
tests/test_world_map_exploration_state.cpp
Normal file
89
tests/test_world_map_exploration_state.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Tests for the extracted world map exploration state module
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/world_map/exploration_state.hpp"
|
||||
#include "rendering/world_map/world_map_types.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace wowee::rendering::world_map;
|
||||
|
||||
static Zone makeZone(uint32_t wmaID, uint32_t areaID,
|
||||
float locLeft, float locRight,
|
||||
float locTop, float locBottom,
|
||||
uint32_t parentWmaID = 0) {
|
||||
Zone z;
|
||||
z.wmaID = wmaID;
|
||||
z.areaID = areaID;
|
||||
z.bounds.locLeft = locLeft;
|
||||
z.bounds.locRight = locRight;
|
||||
z.bounds.locTop = locTop;
|
||||
z.bounds.locBottom = locBottom;
|
||||
z.parentWorldMapID = parentWmaID;
|
||||
return z;
|
||||
}
|
||||
|
||||
TEST_CASE("ExplorationState: initially has no server mask", "[world_map][exploration_state]") {
|
||||
ExplorationState es;
|
||||
REQUIRE(es.hasServerMask() == false);
|
||||
REQUIRE(es.exploredZones().empty());
|
||||
REQUIRE(es.exploredOverlays().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("ExplorationState: setServerMask toggles hasServerMask", "[world_map][exploration_state]") {
|
||||
ExplorationState es;
|
||||
std::vector<uint32_t> mask = {0xFF, 0x00, 0x01};
|
||||
es.setServerMask(mask, true);
|
||||
REQUIRE(es.hasServerMask() == true);
|
||||
|
||||
es.setServerMask({}, false);
|
||||
REQUIRE(es.hasServerMask() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("ExplorationState: overlaysChanged tracks changes", "[world_map][exploration_state]") {
|
||||
ExplorationState es;
|
||||
REQUIRE(es.overlaysChanged() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("ExplorationState: clearLocal resets local data", "[world_map][exploration_state]") {
|
||||
ExplorationState es;
|
||||
es.clearLocal();
|
||||
REQUIRE(es.exploredZones().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("ExplorationState: update with empty zones is safe", "[world_map][exploration_state]") {
|
||||
ExplorationState es;
|
||||
std::vector<Zone> zones;
|
||||
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||
glm::vec3 pos(0.0f);
|
||||
|
||||
es.update(zones, pos, -1, exploreFlagByAreaId);
|
||||
REQUIRE(es.exploredZones().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("ExplorationState: update with valid zone and server mask", "[world_map][exploration_state]") {
|
||||
ExplorationState es;
|
||||
|
||||
std::vector<Zone> zones;
|
||||
auto z = makeZone(1, 100, 1000.0f, -1000.0f, 1000.0f, -1000.0f, 0);
|
||||
z.exploreBits.push_back(0); // bit 0
|
||||
|
||||
OverlayEntry ov;
|
||||
ov.areaIDs[0] = 100;
|
||||
z.overlays.push_back(ov);
|
||||
zones.push_back(z);
|
||||
|
||||
// Set server mask with bit 0 set
|
||||
es.setServerMask({0x01}, true);
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||
exploreFlagByAreaId[100] = 0; // AreaID 100 → explore bit 0
|
||||
|
||||
glm::vec3 playerPos(0.0f, 0.0f, 0.0f);
|
||||
es.update(zones, playerPos, 0, exploreFlagByAreaId);
|
||||
|
||||
// Zone should be explored since bit 0 is set in the mask
|
||||
REQUIRE(es.exploredZones().count(0) == 1);
|
||||
}
|
||||
212
tests/test_world_map_map_resolver.cpp
Normal file
212
tests/test_world_map_map_resolver.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// Tests for the map_resolver module — centralized map navigation resolution.
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/world_map/map_resolver.hpp"
|
||||
#include "rendering/world_map/world_map_types.hpp"
|
||||
|
||||
using namespace wowee::rendering::world_map;
|
||||
|
||||
// ── Helper: build minimal zones for testing ──────────────────
|
||||
|
||||
static Zone makeZone(uint32_t wmaID, uint32_t areaID,
|
||||
const std::string& name = "",
|
||||
uint32_t displayMapID = 0,
|
||||
uint32_t parentWorldMapID = 0) {
|
||||
Zone z;
|
||||
z.wmaID = wmaID;
|
||||
z.areaID = areaID;
|
||||
z.areaName = name;
|
||||
z.displayMapID = displayMapID;
|
||||
z.parentWorldMapID = parentWorldMapID;
|
||||
return z;
|
||||
}
|
||||
|
||||
// Build zone list mimicking Azeroth (mapID=0) with root + leaf continents
|
||||
static std::vector<Zone> buildAzerothZones() {
|
||||
std::vector<Zone> zones;
|
||||
// [0] Root continent (areaID=0, has children → isRootContinent)
|
||||
zones.push_back(makeZone(1, 0, "Azeroth", 0, 0));
|
||||
// [1] Leaf continent for EK (areaID=0, parentWorldMapID=1 → child of root)
|
||||
zones.push_back(makeZone(2, 0, "EasternKingdoms", 0, 1));
|
||||
// [2] Leaf continent for Kalimdor (shouldn't exist on mapID=0, but for testing)
|
||||
zones.push_back(makeZone(3, 0, "Kalimdor", 1, 1));
|
||||
// [3] Regular zone
|
||||
zones.push_back(makeZone(10, 40, "Westfall", 0, 2));
|
||||
// [4] Regular zone
|
||||
zones.push_back(makeZone(11, 44, "Redridge", 0, 2));
|
||||
return zones;
|
||||
}
|
||||
|
||||
// Build zone list with only one continent (no leaf/root distinction)
|
||||
static std::vector<Zone> buildSimpleZones() {
|
||||
std::vector<Zone> zones;
|
||||
// [0] Single continent entry
|
||||
zones.push_back(makeZone(1, 0, "Kalimdor", 1, 0));
|
||||
// [1] Zone
|
||||
zones.push_back(makeZone(10, 331, "Ashenvale", 1, 1));
|
||||
// [2] Zone
|
||||
zones.push_back(makeZone(11, 400, "ThousandNeedles", 1, 1));
|
||||
return zones;
|
||||
}
|
||||
|
||||
// ── mapIdToFolder / folderToMapId / mapDisplayName ───────────
|
||||
|
||||
TEST_CASE("mapIdToFolder: known continent IDs",
|
||||
"[world_map][map_resolver]") {
|
||||
REQUIRE(std::string(mapIdToFolder(0)) == "Azeroth");
|
||||
REQUIRE(std::string(mapIdToFolder(1)) == "Kalimdor");
|
||||
REQUIRE(std::string(mapIdToFolder(530)) == "Expansion01");
|
||||
REQUIRE(std::string(mapIdToFolder(571)) == "Northrend");
|
||||
}
|
||||
|
||||
TEST_CASE("mapIdToFolder: special views",
|
||||
"[world_map][map_resolver]") {
|
||||
REQUIRE(std::string(mapIdToFolder(UINT32_MAX)) == "World");
|
||||
REQUIRE(std::string(mapIdToFolder(UINT32_MAX - 1)) == "Cosmic");
|
||||
}
|
||||
|
||||
TEST_CASE("mapIdToFolder: unknown returns empty",
|
||||
"[world_map][map_resolver]") {
|
||||
REQUIRE(std::string(mapIdToFolder(9999)) == "");
|
||||
}
|
||||
|
||||
TEST_CASE("folderToMapId: case-insensitive lookup",
|
||||
"[world_map][map_resolver]") {
|
||||
REQUIRE(folderToMapId("Azeroth") == 0);
|
||||
REQUIRE(folderToMapId("azeroth") == 0);
|
||||
REQUIRE(folderToMapId("KALIMDOR") == 1);
|
||||
REQUIRE(folderToMapId("Northrend") == 571);
|
||||
REQUIRE(folderToMapId("world") == static_cast<int>(UINT32_MAX));
|
||||
REQUIRE(folderToMapId("unknown") == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("mapDisplayName: returns UI labels",
|
||||
"[world_map][map_resolver]") {
|
||||
REQUIRE(std::string(mapDisplayName(0)) == "Eastern Kingdoms");
|
||||
REQUIRE(std::string(mapDisplayName(1)) == "Kalimdor");
|
||||
REQUIRE(std::string(mapDisplayName(571)) == "Northrend");
|
||||
REQUIRE(mapDisplayName(9999) == nullptr);
|
||||
}
|
||||
|
||||
// ── findContinentForMapId ────────────────────────────────────
|
||||
|
||||
TEST_CASE("findContinentForMapId: prefers leaf continent with matching displayMapID",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
int idx = findContinentForMapId(zones, 0, -1);
|
||||
REQUIRE(idx == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("findContinentForMapId: finds leaf by displayMapID=1 for Kalimdor",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
int idx = findContinentForMapId(zones, 1, -1);
|
||||
REQUIRE(idx == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("findContinentForMapId: falls back to first non-root continent",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildSimpleZones();
|
||||
int idx = findContinentForMapId(zones, 999, -1);
|
||||
REQUIRE(idx == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("findContinentForMapId: skips cosmic zone index",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
int idx = findContinentForMapId(zones, 0, 1);
|
||||
REQUIRE(idx == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("findContinentForMapId: returns -1 for empty zones",
|
||||
"[world_map][map_resolver]") {
|
||||
std::vector<Zone> empty;
|
||||
int idx = findContinentForMapId(empty, 0, -1);
|
||||
REQUIRE(idx == -1);
|
||||
}
|
||||
|
||||
// ── resolveWorldRegionClick ──────────────────────────────────
|
||||
|
||||
TEST_CASE("resolveWorldRegionClick: same map returns NAVIGATE_CONTINENT",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
auto result = resolveWorldRegionClick(0, zones, 0, -1);
|
||||
REQUIRE(result.action == MapResolveAction::NAVIGATE_CONTINENT);
|
||||
REQUIRE(result.targetZoneIdx == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveWorldRegionClick: different map returns LOAD_MAP",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
auto result = resolveWorldRegionClick(1, zones, 0, -1);
|
||||
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||
REQUIRE(result.targetMapName == "Kalimdor");
|
||||
}
|
||||
|
||||
TEST_CASE("resolveWorldRegionClick: Northrend from Azeroth returns LOAD_MAP",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
auto result = resolveWorldRegionClick(571, zones, 0, -1);
|
||||
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||
REQUIRE(result.targetMapName == "Northrend");
|
||||
}
|
||||
|
||||
TEST_CASE("resolveWorldRegionClick: unknown mapId returns NONE",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
auto result = resolveWorldRegionClick(9999, zones, 0, -1);
|
||||
REQUIRE(result.action == MapResolveAction::NONE);
|
||||
}
|
||||
|
||||
// ── resolveZoneClick ─────────────────────────────────────────
|
||||
|
||||
TEST_CASE("resolveZoneClick: normal zone returns ENTER_ZONE",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
auto result = resolveZoneClick(3, zones, 0);
|
||||
REQUIRE(result.action == MapResolveAction::ENTER_ZONE);
|
||||
REQUIRE(result.targetZoneIdx == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveZoneClick: zone with different displayMapID returns LOAD_MAP",
|
||||
"[world_map][map_resolver]") {
|
||||
std::vector<Zone> zones;
|
||||
zones.push_back(makeZone(1, 0, "Azeroth", 0, 0));
|
||||
zones.push_back(makeZone(50, 100, "DarkPortal", 530, 1));
|
||||
|
||||
auto result = resolveZoneClick(1, zones, 0);
|
||||
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||
REQUIRE(result.targetMapName == "Expansion01");
|
||||
}
|
||||
|
||||
TEST_CASE("resolveZoneClick: zone with displayMapID matching current returns ENTER_ZONE",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildSimpleZones();
|
||||
auto result = resolveZoneClick(1, zones, 1);
|
||||
REQUIRE(result.action == MapResolveAction::ENTER_ZONE);
|
||||
REQUIRE(result.targetZoneIdx == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveZoneClick: out of range returns NONE",
|
||||
"[world_map][map_resolver]") {
|
||||
auto zones = buildAzerothZones();
|
||||
auto result = resolveZoneClick(-1, zones, 0);
|
||||
REQUIRE(result.action == MapResolveAction::NONE);
|
||||
|
||||
result = resolveZoneClick(99, zones, 0);
|
||||
REQUIRE(result.action == MapResolveAction::NONE);
|
||||
}
|
||||
|
||||
// ── resolveCosmicClick ───────────────────────────────────────
|
||||
|
||||
TEST_CASE("resolveCosmicClick: returns LOAD_MAP for known mapId",
|
||||
"[world_map][map_resolver]") {
|
||||
auto result = resolveCosmicClick(530);
|
||||
REQUIRE(result.action == MapResolveAction::LOAD_MAP);
|
||||
REQUIRE(result.targetMapName == "Expansion01");
|
||||
}
|
||||
|
||||
TEST_CASE("resolveCosmicClick: returns NONE for unknown mapId",
|
||||
"[world_map][map_resolver]") {
|
||||
auto result = resolveCosmicClick(9999);
|
||||
REQUIRE(result.action == MapResolveAction::NONE);
|
||||
}
|
||||
198
tests/test_world_map_view_state_machine.cpp
Normal file
198
tests/test_world_map_view_state_machine.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// Tests for the extracted world map view state machine module
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/world_map/view_state_machine.hpp"
|
||||
|
||||
using namespace wowee::rendering::world_map;
|
||||
|
||||
TEST_CASE("ViewStateMachine: initial state is CONTINENT", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::CONTINENT);
|
||||
REQUIRE(sm.continentIdx() == -1);
|
||||
REQUIRE(sm.currentZoneIdx() == -1);
|
||||
REQUIRE(sm.transition().active == false);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomIn from CONTINENT to ZONE", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
sm.setContinentIdx(0);
|
||||
|
||||
auto result = sm.zoomIn(5, 5);
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::ZONE);
|
||||
REQUIRE(result.targetIdx == 5);
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::ZONE);
|
||||
REQUIRE(sm.currentZoneIdx() == 5);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomIn from CONTINENT with no zone does nothing", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
sm.setContinentIdx(0);
|
||||
|
||||
auto result = sm.zoomIn(-1, -1);
|
||||
REQUIRE(result.changed == false);
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::CONTINENT);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomOut from ZONE to CONTINENT", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::ZONE);
|
||||
sm.setContinentIdx(0);
|
||||
sm.setCurrentZoneIdx(5);
|
||||
|
||||
auto result = sm.zoomOut();
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::CONTINENT);
|
||||
REQUIRE(result.targetIdx == 0);
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::CONTINENT);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomOut from ZONE without continent does nothing", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::ZONE);
|
||||
sm.setContinentIdx(-1);
|
||||
|
||||
auto result = sm.zoomOut();
|
||||
REQUIRE(result.changed == false);
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::ZONE);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomOut from CONTINENT to WORLD", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
|
||||
auto result = sm.zoomOut();
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::WORLD);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomOut from WORLD to COSMIC when enabled", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::WORLD);
|
||||
sm.setCosmicEnabled(true);
|
||||
|
||||
auto result = sm.zoomOut();
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::COSMIC);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomOut from WORLD stays when cosmic disabled", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::WORLD);
|
||||
sm.setCosmicEnabled(false);
|
||||
|
||||
auto result = sm.zoomOut();
|
||||
REQUIRE(result.changed == false);
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::WORLD);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomIn from COSMIC goes to WORLD", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::COSMIC);
|
||||
sm.setCosmicEnabled(true);
|
||||
|
||||
auto result = sm.zoomIn(-1, -1);
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomIn from WORLD to CONTINENT with continent set", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::WORLD);
|
||||
sm.setContinentIdx(3);
|
||||
|
||||
auto result = sm.zoomIn(-1, -1);
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::CONTINENT);
|
||||
REQUIRE(result.targetIdx == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: enterWorldView sets WORLD level", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::ZONE);
|
||||
|
||||
auto result = sm.enterWorldView();
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||
REQUIRE(sm.currentLevel() == ViewLevel::WORLD);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: enterCosmicView when disabled falls back to WORLD", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setCosmicEnabled(false);
|
||||
|
||||
auto result = sm.enterCosmicView();
|
||||
REQUIRE(result.newLevel == ViewLevel::WORLD);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: enterZone goes to ZONE level", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
|
||||
auto result = sm.enterZone(7);
|
||||
REQUIRE(result.changed == true);
|
||||
REQUIRE(result.newLevel == ViewLevel::ZONE);
|
||||
REQUIRE(result.targetIdx == 7);
|
||||
REQUIRE(sm.currentZoneIdx() == 7);
|
||||
}
|
||||
|
||||
// ── Transition animation ─────────────────────────────────────
|
||||
|
||||
TEST_CASE("ViewStateMachine: transition starts on zoom", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
sm.setContinentIdx(0);
|
||||
sm.zoomIn(5, 5);
|
||||
|
||||
REQUIRE(sm.transition().active == true);
|
||||
REQUIRE(sm.transition().progress == Catch::Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: updateTransition advances progress", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
sm.setContinentIdx(0);
|
||||
sm.zoomIn(5, 5);
|
||||
|
||||
float halfDuration = sm.transition().duration / 2.0f;
|
||||
bool stillActive = sm.updateTransition(halfDuration);
|
||||
REQUIRE(stillActive == true);
|
||||
REQUIRE(sm.transition().progress == Catch::Approx(0.5f).margin(0.01f));
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: transition completes after full duration", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
sm.setContinentIdx(0);
|
||||
sm.zoomIn(5, 5);
|
||||
|
||||
float dur = sm.transition().duration;
|
||||
sm.updateTransition(dur + 0.1f); // overshoot
|
||||
REQUIRE(sm.transition().active == false);
|
||||
REQUIRE(sm.transition().progress == Catch::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: updateTransition when no transition returns false", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
REQUIRE(sm.updateTransition(0.1f) == false);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomIn prefers hovered zone over player zone", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
sm.setContinentIdx(0);
|
||||
|
||||
auto result = sm.zoomIn(/*hovered=*/3, /*player=*/7);
|
||||
REQUIRE(result.targetIdx == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("ViewStateMachine: zoomIn falls back to player zone when no hover", "[world_map][view_state_machine]") {
|
||||
ViewStateMachine sm;
|
||||
sm.setLevel(ViewLevel::CONTINENT);
|
||||
sm.setContinentIdx(0);
|
||||
|
||||
auto result = sm.zoomIn(/*hovered=*/-1, /*player=*/7);
|
||||
REQUIRE(result.targetIdx == 7);
|
||||
}
|
||||
86
tests/test_world_map_zone_metadata.cpp
Normal file
86
tests/test_world_map_zone_metadata.cpp
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Tests for the extracted world map zone metadata module
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/world_map/zone_metadata.hpp"
|
||||
#include "rendering/world_map/world_map_types.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace wowee::rendering::world_map;
|
||||
|
||||
TEST_CASE("ZoneMetadata: find returns nullptr for unknown zone", "[world_map][zone_metadata]") {
|
||||
ZoneMetadata zm;
|
||||
zm.initialize();
|
||||
REQUIRE(zm.find("NonexistentZoneXYZ") == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: find returns valid data for known zones", "[world_map][zone_metadata]") {
|
||||
ZoneMetadata zm;
|
||||
zm.initialize();
|
||||
|
||||
const ZoneMeta* elwynn = zm.find("Elwynn");
|
||||
REQUIRE(elwynn != nullptr);
|
||||
REQUIRE(elwynn->minLevel > 0);
|
||||
REQUIRE(elwynn->maxLevel >= elwynn->minLevel);
|
||||
REQUIRE(elwynn->faction == ZoneFaction::Alliance);
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: Contested zones", "[world_map][zone_metadata]") {
|
||||
ZoneMetadata zm;
|
||||
zm.initialize();
|
||||
|
||||
const ZoneMeta* sTV = zm.find("StranglethornVale");
|
||||
REQUIRE(sTV != nullptr);
|
||||
REQUIRE(sTV->faction == ZoneFaction::Contested);
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: Horde zones", "[world_map][zone_metadata]") {
|
||||
ZoneMetadata zm;
|
||||
zm.initialize();
|
||||
|
||||
const ZoneMeta* durotar = zm.find("Durotar");
|
||||
REQUIRE(durotar != nullptr);
|
||||
REQUIRE(durotar->faction == ZoneFaction::Horde);
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: formatLabel with no metadata", "[world_map][zone_metadata]") {
|
||||
std::string label = ZoneMetadata::formatLabel("UnknownZone", nullptr);
|
||||
REQUIRE(label == "UnknownZone");
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: formatLabel with metadata", "[world_map][zone_metadata]") {
|
||||
ZoneMeta meta;
|
||||
meta.minLevel = 10;
|
||||
meta.maxLevel = 20;
|
||||
meta.faction = ZoneFaction::Alliance;
|
||||
|
||||
std::string label = ZoneMetadata::formatLabel("Elwynn", &meta);
|
||||
// Should contain the zone name
|
||||
REQUIRE(label.find("Elwynn") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: formatHoverLabel with metadata", "[world_map][zone_metadata]") {
|
||||
ZoneMeta meta;
|
||||
meta.minLevel = 30;
|
||||
meta.maxLevel = 40;
|
||||
meta.faction = ZoneFaction::Contested;
|
||||
|
||||
std::string label = ZoneMetadata::formatHoverLabel("StranglethornVale", &meta);
|
||||
// Should contain both zone name and level range
|
||||
REQUIRE(label.find("StranglethornVale") != std::string::npos);
|
||||
REQUIRE(label.find("30") != std::string::npos);
|
||||
REQUIRE(label.find("40") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: formatHoverLabel with no metadata just returns name", "[world_map][zone_metadata]") {
|
||||
std::string label = ZoneMetadata::formatHoverLabel("UnknownZone", nullptr);
|
||||
REQUIRE(label == "UnknownZone");
|
||||
}
|
||||
|
||||
TEST_CASE("ZoneMetadata: double initialization is safe", "[world_map][zone_metadata]") {
|
||||
ZoneMetadata zm;
|
||||
zm.initialize();
|
||||
zm.initialize(); // should not crash or change data
|
||||
|
||||
const ZoneMeta* elwynn = zm.find("Elwynn");
|
||||
REQUIRE(elwynn != nullptr);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue