Kelsidavis-WoWee/tests/test_world_map_view_state_machine.cpp
Pavel Okhlopkov fff06fc932 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>
2026-04-12 09:52:51 +03:00

198 lines
6.7 KiB
C++

// 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);
}