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
- Meaningful Choices - Players choose which nodes to tackle, not which path is randomly available
- Domain Identity - Each region has distinct visual/gameplay identity through domain configuration
- Configuration Inheritance - Nodes inherit from domains unless explicitly overridden
- 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)
┌──────────────────────────────────────────────────────────────────┐
│ 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.
- 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) |
| 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 |