Skip to content

World Map System

Summary: The World Map System manages The Chasm's fixed-topology navigation through UWorldMapSubsystem, a GameInstance subsystem that handles node travel, domain progression, and level streaming. Designers author the map structure via UWorldMapData data assets, while runtime state (node unlocks, domain liberation) is tracked server-authoritatively.

Table of Contents


Why This Design

Designer-Authored vs Procedural

The World Map uses a fixed topology approach rather than procedural generation:

Approach World Map Dungeons
Structure Designer-authored nodes Procedurally generated rooms
Connections Fixed, bidirectional Template-based
Configuration Node override or domain default Per-domain room pools

Design Goals

  1. Meaningful Choices - Players choose which nodes to tackle, not which path is randomly available
  2. Domain Identity - Each region has distinct visual/gameplay identity through domain configuration
  3. Configuration Inheritance - Nodes inherit from domains unless explicitly overridden
  4. Server Authority - All state changes originate on server for multiplayer consistency

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        UWorldMapData                                 │
│  (Data Asset - Designer Authored)                                   │
│  ┌───────────────────┐  ┌───────────────────┐                       │
│  │ TArray<FWorldMap- │  │ TArray<FDomain-   │                       │
│  │       Node>       │  │       Region>     │                       │
│  └─────────┬─────────┘  └─────────┬─────────┘                       │
└────────────┼──────────────────────┼─────────────────────────────────┘
             │                      │
             ▼                      ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    UWorldMapSubsystem                                │
│  (GameInstance Subsystem - Runtime Manager)                         │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ FWorldMapState                                                 │  │
│  │  • TMap<FName, FWorldMapNode> Nodes                           │  │
│  │  • TMap<FGameplayTag, FDomainRegion> Domains                  │  │
│  │  • FName CurrentNodeID                                        │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                      │
│  Methods: LoadWorldMap, TravelToNode, MarkNodeCleared, etc.         │
└──────────────────────────────────┬──────────────────────────────────┘
          ┌────────────────────────┼────────────────────────┐
          ▼                        ▼                        ▼
┌─────────────────┐  ┌─────────────────────────┐  ┌─────────────────┐
│ UDungeonSub-    │  │ ULevelStreamingDynamic  │  │ UWorldMap-      │
│ system          │  │ (Fixed Level Streaming) │  │ Controller      │
│ (Dungeon Gen)   │  │                         │  │ (MVVM UI)       │
└─────────────────┘  └─────────────────────────┘  └─────────────────┘

Component Ownership

Component Owner Why
UWorldMapSubsystem GameInstance Persists across level transitions
UWorldMapController UISubsystem MVVM controller for map widget
UWorldMapData DataAsset Static designer-authored content

Core Concepts

FWorldMapNode

A single location on the world map. Nodes are connected bidirectionally.

Field Type Purpose
NodeID FName Unique identifier
DisplayName FText Shown to player
NodeType EWorldNodeType Determines behavior (Hub, Combat, etc.)
Depth int32 Tier on map (0 = start area)
DomainTag FGameplayTag Which domain controls this node
ConnectedNodeIDs TArray\<FName> Adjacent nodes
MapPosition FVector2D UI position (normalized 0-1)
Tagline FText Short atmospheric flavor text
Encounter TSoftObjectPtr Enemy pool override
DungeonConfigOverride TSoftObjectPtr Topology/room pool override
ModifierPoolOverride TSoftObjectPtr Area modifier pool override
RemnantRealmChanceOverride float Chance [0..1] this node's dungeon grows a Remnant Realm room; -1 (default) = inherit the domain config
ItemLevelRangeOverride FIntPoint Item level range (0,0 = auto from depth)
FixedLevel TSoftObjectPtr\<UWorld> Level to stream for non-dungeon nodes
DepthOverride int32 Gameplay-only depth override (0 = use Depth)

Remnant Realm Chance Resolution

RemnantRealmChanceOverride uses a negative sentinel rather than a companion bool: HasRemnantRealmChanceOverride() is simply >= 0.0f, so -1 means "inherit" and 0.0 is a meaningful "explicitly never". UWorldMapData::GetEffectiveRemnantRealmChance(Node) resolves it:

GetEffectiveRemnantRealmChance(Node)
    ├─ !Node.GeneratesDungeon()          → 0.0
    ├─ Node.HasRemnantRealmChanceOverride() → Node.RemnantRealmChanceOverride
    ├─ GetEffectiveDungeonConfig(Node)      → Config->RemnantRealmChance
    └─ otherwise                            → 0.0

WorldMap_Main sets Ashwood to 1.0 as a demo guarantee — that node's dungeon always grows a Remnant Realm room. See Dungeon System for what the chance actually places.

Unlocking is adjacency-only: clearing a node unlocks its ConnectedNodeIDs. There is no prerequisite/AND-gating field (RULING-7, WorldSystemsScalingReview.plan.md) — vertical descent plus HQ liberation already gate progression; add prerequisites only if a specific map design demands them.

FDomainRegion

A themed region containing multiple nodes.

Field Type Purpose
DomainTag FGameplayTag Domain identifier (e.g., Domain.Vorath)
DomainName FText Display name
DomainColor FLinearColor UI theming color
MinDepth / MaxDepth int32 Depth range
HQNodeID FName Domain boss node
DefaultDungeonConfig TSoftObjectPtr Default config for nodes in domain
bIsLiberated bool Runtime: boss defeated

FWorldMapState

Runtime state containing all nodes and domains with their current progression.

Field Type Purpose
Nodes TMap\<FName, FWorldMapNode> All nodes with runtime state
Domains TMap\<FGameplayTag, FDomainRegion> All domains with runtime state
CurrentNodeID FName Player's current location

Node Types and States

Node Types

Type Generates Dungeon Description
Hub No Safe zone, services
Camp No Rest site / checkpoint
Start No Map entry point
Event No Story/dialogue node
End No Final destination
Combat Yes Standard dungeon
Elite Yes Harder dungeon
Treasure Yes Loot-focused dungeon
DomainHQ Yes Boss dungeon

Node State Flow

                    ┌──────────────────┐
                    │      Locked      │
                    │ (No adjacent     │
                    │  cleared node)   │
                    └────────┬─────────┘
                             │ adjacent node cleared
                    ┌──────────────────┐
                    │    Available     │
                    │ (Can travel to)  │
                    └────────┬─────────┘
                             │ TravelToNode()
                    ┌──────────────────┐
                    │     Current      │
                    │ (Player is here) │
                    └────────┬─────────┘
                             │ MarkNodeCleared()
                    ┌──────────────────┐
                    │     Cleared      │
                    │ (Completed)      │
                    └──────────────────┘

Helper Methods on FWorldMapNode

Method Returns Description
IsHubNode() bool Hub, Camp, Start, or Event
GeneratesDungeon() bool Combat, Elite, Treasure, DomainHQ
IsHQNode() bool DomainHQ only
IsSafeZone() bool Hub or Camp
HasConfigOverride() bool Has explicit dungeon config
HasModifierPoolOverride() bool Has explicit modifier pool
GetItemLevelRange() FIntPoint Override or depth-based range
GetHiddenModifierCount() int32 Unrevealed modifiers
GetHiddenThreatCount() int32 Unrevealed threats
IsFullyRevealed() bool All intel revealed
HasFixedLevel() bool Has level to stream

Domain System

Domain Liberation

Domains are "liberated" when their HQ boss is defeated:

TravelToNode(HQNode)
Generate HQ Dungeon (with boss)
Player defeats boss
MarkNodeCleared(HQNode)
LiberateDomain(DomainTag)
OnDomainLiberated broadcast

Domain Queries

Method Purpose
IsDomainLiberated(Tag) Check if domain boss defeated
GetNodesInDomain(Tag) Get all nodes in a domain
GetDomainColorForNode(Node) Get theming color

Configuration Resolution

Dungeon-generating nodes inherit configuration from their domain unless overridden:

GetEffectiveDungeonConfig(Node)
    ├─ Node.GeneratesDungeon() == false?
    │       └─ Return nullptr
    ├─ Node.DungeonConfigOverride set?
    │       └─ Return Node.DungeonConfigOverride
    └─ Domain.DefaultDungeonConfig set?
            └─ Return Domain.DefaultDungeonConfig
            └─ Otherwise: Return nullptr

Configuration Source Tracking

For debugging/auditing, UWorldMapData provides:

Method Returns Purpose
GetDungeonConfigSource(Node) EDungeonConfigSource None, DomainDefault, NodeOverride, Missing
GetEncounterSource(Node) EEncounterSource None, DomainDefault, NodeDirect, Missing

Level Streaming

Non-dungeon nodes (Hub, Camp, Event, Start, End) stream a fixed level:

TravelToNode(NodeID)
    ├─ Node.GeneratesDungeon()?
    │       └─ Trigger DungeonSubsystem
    └─ Node.HasFixedLevel()?
    StreamInFixedLevel(NodeID, Level)
        ├─ Unload previous level (if any)
        ├─ ULevelStreamingDynamic::LoadLevelInstance()
        └─ OnLevelStreamingComplete()
        OnFixedLevelStreamingComplete broadcast

Streaming State

Property Type Purpose
CurrentStreamedLevel ULevelStreamingDynamic* Active streamed level
CurrentStreamedNodeID FName Node that owns streamed level
bIsStreamingInProgress bool Streaming operation active

Streaming Events

Delegate When Fired
OnFixedLevelEntered Starting to stream a fixed level
OnFixedLevelStreamingComplete Level fully loaded and visible
OnFixedLevelUnloaded Previous level unloaded

UI Layer (MVVM)

Controller → ViewModel → Widget

┌──────────────────────────────────────────────────────────────────┐
│                    UWorldMapController                            │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │ • BindToWorldMapSubsystem()                                 │  │
│  │ • SelectNode(NodeID)                                        │  │
│  │ • ConfirmTravel()                                           │  │
│  │ • RefreshAllNodes()                                         │  │
│  └─────────────────────────────────┬──────────────────────────┘  │
└────────────────────────────────────┼─────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│                    UWorldMapViewModel                             │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │ • SelectedNodeID                                            │  │
│  │ • CanTravelToSelected                                       │  │
│  │ • NodeUIDataArray                                           │  │
│  │ • ConnectionDataArray                                       │  │
│  │ • NodeInfoPanelData (modifiers, threats, item level)        │  │
│  │ • bNodeInfoPanelVisible                                     │  │
│  └─────────────────────────────────┬──────────────────────────┘  │
└────────────────────────────────────┼─────────────────────────────┘
                                     │ FieldNotify bindings
┌──────────────────────────────────────────────────────────────────┐
│                       UMG Widgets                                 │
│  ┌────────────────────────────┐  ┌──────────────────────────┐    │
│  │ UWorldMapWidget            │  │ UNodeInfoPanelWidget     │    │
│  │ (Nodes, connections, map)  │  │ (Modifiers, threats,     │    │
│  │                            │  │  item level, action btn) │    │
│  └────────────────────────────┘  └──────────────────────────┘    │
└──────────────────────────────────────────────────────────────────┘

Controller Event Handlers

Handler Reacts To
OnWorldMapLoaded Map data loaded
OnNodeStateChanged Node state transition
OnCurrentNodeChanged Player moved to new node
OnDomainLiberated Domain boss defeated

Layout Contract

Node layout geometry is not stored on UWorldMapController. It lives in a single shared header, WorldMapLayoutContract.h: a WorldMapLayout namespace of inline constexpr constants plus a stateless FWorldMapLayoutContract transform struct. Both the runtime widget (UWorldMapController) and the editor preview panel (SWorldMapPreviewPanel) compute positions through it, so the in-game map and the editor preview cannot drift.

Position Formula

  • Y (vertical) is purely depth-derived: Y = TopMargin + Depth * DepthSpacing. MapPosition.Y is unused — depth owns the vertical axis.
  • X (horizontal): if Node.bHasMapPosition, replay the authored MapPosition.X via FWorldMapLayoutContract::MapPositionToCanvas; otherwise use FallbackX(IndexInDepth, NodesAtDepth) (a centered per-band spread). Then add OrganicOffsetX(NodeID) (deterministic; 0 while jitter is disabled).

Runtime implementation: UWorldMapController::CalculateNodeCanvasPosition (WorldMapController.cpp).

WorldMapLayout Constants

Constant Value Purpose
CanvasWidth 1200.0f Fixed design-space canvas width (px); each surface scales it to fit
TopMargin 80.0f Px from canvas top to the Depth-0 row center
DepthSpacing 150.0f Px between depth rows (equals the editor graph lane height)
HorizontalMargin 0.10f Fraction of CanvasWidth reserved as empty side margin
NodeSpacingFraction 0.34f Normalized spacing between adjacent nodes in a depth band
OrganicJitterX 0.0f Max deterministic per-node horizontal jitter (0 = off; placement is hand-authored)
NodeSize (200, 52) Node plaque footprint (px, design space) — preview draws rects at this size (WYSIWYG)

FWorldMapLayoutContract Transforms

Method Purpose
MapPositionToCanvas(MapPosition, Depth) Authored normalized position + depth → canvas pixel center
CanvasToMapPosition(CanvasPos, Depth) Inverse: canvas pixel + depth lane → normalized MapPosition (Y = 0)
CanvasYToDepth(CanvasY) Canvas Y → nearest depth lane (the drag snap rule; shared with the graph)
CenteredSpreadFraction(Index, Count) Normalized X for a node's rank within its depth band (auto-centered)
FallbackX(Index, Count) Canvas px center X for a node's band rank (used when bHasMapPosition is false)
OrganicOffsetX(NodeID) Deterministic per-node offset seeded by NodeID; returns 0 while jitter is off

Authoring (Map Editor)

The map is authored in the single-canvas map editor (Option B, merged 2026-06-30), a WYSIWYG canvas (SWorldMapPreviewPanel) that replaced the old dual graph/preview workflow: lane-based depth layout, connection draw, node create/delete, and click-select into the Details panel. Because the canvas shares FWorldMapLayoutContract with runtime, what a designer drags is exactly what ships. Dragging a node writes MapPosition (via CanvasToMapPosition) and sets bHasMapPosition = true; a vertical drag snaps to a depth lane via CanvasYToDepth. The legacy node-graph tab (WorldMapGraph) is demoted to a debug fallback.


Node Intel System

Dungeon-generating nodes have area modifiers and threat information that is progressively revealed to the player.

Intel Population

When a node transitions to Available, PopulateNodeIntel() rolls modifiers and extracts threats:

PopulateNodeIntel(NodeID)
    ├─ Get modifier pool (node override → domain config)
    │       └─ Register pool with ModifierSubsystem (unified lookup)
    ├─ Roll modifiers deterministically
    │       └─ Seed = GetTypeHash(NodeID) → consistent across restarts
    ├─ Extract threats from encounter
    │       └─ Encounter->GetUniqueEnemyTypes()
    └─ UpdateNodeRevelation() → set what's visible

Revelation Rules

Node State Modifiers Revealed Threats Revealed
Never visited 0 0
Visited (not cleared) ~half (min 1) ~half (min 1)
Cleared once All All
Domain HQ Increases with attempts Increases with attempts

Runtime Intel Fields on FWorldMapNode

Field Type Purpose
RolledModifiers TArray\<FRolledModifier> Area modifiers for current descent
RevealedModifierCount int32 How many modifiers are visible
RevealedThreats TArray\<FRevealedThreat> Enemy types revealed
TotalThreatCount int32 Total threats (for showing "???")

Dungeon Integration

When traveling to a dungeon-generating node:

TravelToNode(NodeID)
Node.GeneratesDungeon() == true
GetEffectiveDungeonConfig(Node)
DungeonSubsystem->SetAreaContext(Depth, Encounter, RolledModifiers)
DungeonSubsystem->GenerateDungeon(Config, bIsHQDungeon)
    │   (also registers modifier pool with ModifierSubsystem)
OnDungeonGenerated → AEternalDungeonActor begins plugin generation

Exit Flow

Player reaches Exit/Boss room and clears it
DungeonSubsystem->OnDungeonCleared
WorldMapSubsystem->MarkNodeCleared(CurrentNodeID)
UpdateUnlockedNodes() → Connected nodes become Available
(If HQ node) LiberateDomain(DomainTag)

Public Contracts

Methods

Method Parameters Purpose
LoadWorldMap UWorldMapData* Initialize from data asset
ClearWorldMap - Unload current map
GetCurrentNode - Get player's current node
GetNode FName NodeID Get node by ID
GetAccessibleNodeIDs - Get all travelable node IDs
CanTravelToNode FName NodeID Check if travel is valid
TravelToNode FName NodeID Move player to node
MarkNodeCleared FName NodeID Mark node as completed
IsDomainLiberated FGameplayTag Check domain liberation
LiberateDomain FGameplayTag Mark domain boss defeated
UnloadCurrentFixedLevel - Manually unload streamed level

Delegates

Delegate Payload When Fired
OnWorldMapLoaded UWorldMapData* Map data loaded
OnNodeStateChanged FName, EWorldNodeState Any node state changes
OnCurrentNodeChanged FName Player moved to new node
OnDomainLiberated FGameplayTag Domain boss defeated
OnFixedLevelEntered FName, TSoftObjectPtr<UWorld> Starting level stream
OnFixedLevelStreamingComplete FName Level fully loaded
OnFixedLevelUnloaded FName Level unloaded

Source References

Core Types

  • FWorldMapNode - Source/ProjectEternal/Public/WorldMap/WorldMapTypes.h:56
  • FDomainRegion - Source/ProjectEternal/Public/WorldMap/WorldMapTypes.h:170
  • FWorldMapState - Source/ProjectEternal/Public/WorldMap/WorldMapTypes.h:217
  • EWorldNodeType - Source/ProjectEternal/Public/WorldMap/WorldMapTypes.h:14
  • EWorldNodeState - Source/ProjectEternal/Public/WorldMap/WorldMapTypes.h:39

Subsystem

  • UWorldMapSubsystem - Source/ProjectEternal/Public/WorldMap/WorldMapSubsystem.h:28

Data Asset

  • UWorldMapData - Source/ProjectEternal/Public/WorldMap/WorldMapData.h:48
  • EDungeonConfigSource - Source/ProjectEternal/Public/WorldMap/WorldMapData.h:15
  • EEncounterSource - Source/ProjectEternal/Public/WorldMap/WorldMapData.h:28

UI

  • UWorldMapController - Source/ProjectEternal/Public/UI/Controllers/WorldMapController.h
  • UWorldMapViewModel - Source/ProjectEternal/Public/UI/ViewModels/WorldMap/WorldMapViewModel.h
  • UNodeInfoPanelWidget - Source/ProjectEternal/Public/UI/Widgets/WorldMap/NodeInfoPanelWidget.h
  • FNodeInfoPanelData - Source/ProjectEternal/Public/UI/ViewModels/WorldMap/WorldMapViewModel.h

Layout Contract

  • FWorldMapLayoutContract / WorldMapLayout - Source/ProjectEternal/Public/WorldMap/WorldMapLayoutContract.h
  • Contract implementation - Source/ProjectEternal/Private/WorldMap/WorldMapLayoutContract.cpp
  • SWorldMapPreviewPanel (editor canvas) - Source/ProjectEternalEditor/Public/MapEditor/Slate/WorldMap/SWorldMapPreviewPanel.h

Display Types

  • FRevealedThreat - Source/ProjectEternal/Public/AI/AIDisplayTypes.h


Recent Changes

Date Change Impact
2026-08-06 Per-node Remnant Realm chance override FWorldMapNode::RemnantRealmChanceOverride (-1 = inherit) resolved by UWorldMapData::GetEffectiveRemnantRealmChance in node > domain > none order; WorldMap_Main pins Ashwood to 1.0 as a demo guarantee
2026-06-30 Layout contract Node positions computed via shared FWorldMapLayoutContract / WorldMapLayout constants; runtime (UWorldMapController) and editor preview (SWorldMapPreviewPanel) share it so they cannot drift
2026-06-30 Single-canvas map editor WYSIWYG canvas replaces the dual graph/preview workflow; the four node override fields (Encounter/DungeonConfigOverride/ModifierPoolOverride/ItemLevelRangeOverride) now round-trip persist through the Details panel
2026-06-30 Graph tab demoted Legacy WorldMapGraph node-graph editor kept only as a debug fallback
2026-02 Node intel system Area modifiers and threat revelation on dungeon nodes
2026-02 Node info panel (MVVM) UNodeInfoPanelWidget with modifier/threat display via FNodeInfoPanelData
2026-02 Unified modifier lookup Dungeon pools registered with ModifierSubsystem for shared GetDefinition()
2025-01-17 Initial documentation Document world map system architecture