Skip to content

Remnant Item System

Summary: Remnant Items are narrative-tied uniques rolled inside sealed Remnant Realm sub-levels. Players interact with a world-placed portal → enter a streamed realm → complete a Hold-the-Ground event → receive a sealed item with hidden Echo Mods and a Desire progression task → equip the item and fulfil the Desire through play → the item auto-awakens, revealing and activating its Echo Mods. The system is server-authoritative, rides the existing item / equipment / modifier pipelines, and uses per-instance state modules on UItemObject for the Sealed → Awakened lifecycle.

Table of Contents


Architecture Overview

┌────────────────────────────────────────────────────────────────┐
│                     WORLD (persistent)                         │
│                                                                │
│    ARemnantPortalActor ──── UInteractableComponent             │
│         │                                                      │
└─────────┼──────────────────────────────────────────────────────┘
          │ OnInteracted (server)
┌────────────────────────────────────────────────────────────────┐
│                  URemnantRealmSubsystem                        │
│  • WorldSubsystem — lifetime scoped to UWorld                  │
│  • Caches entry transform per player                           │
│  • Builds FTransitionRequest (InWorldTeleport)                 │
└─────────┬──────────────────────────────────────────────────────┘
          │ StartTransition
┌────────────────────────────────────────────────────────────────┐
│              UTransitionStateManager                           │
│  • Multicast freeze + fade                                     │
│  • OnRequestDynamicSublevelLoad → subsystem handler            │
│  • Client-ready tracking for coop                              │
└─────────┬──────────────────────────────────────────────────────┘
          │ ULevelStreamingDynamic::LoadLevelInstance
┌────────────────────────────────────────────────────────────────┐
│                 REALM SUB-LEVEL (streamed)                     │
│                                                                │
│   ARemnantEventActor_HoldGround                                │
│      │ leash tick + progress timer                             │
│      └── ARemnantWaveSpawner ── AEnemySpawner[]                │
│                                                                │
│   On success → URemnantItemFactory::RollRemnantItem            │
│              → ALootContainer dropped                          │
└─────────┬──────────────────────────────────────────────────────┘
          │ pickup → inventory
┌────────────────────────────────────────────────────────────────┐
│                 PLAYER (persistent)                            │
│                                                                │
│   UItemObject (sealed Remnant)                                 │
│      ├── FRemnantFragment (marker, in manifest)                │
│      └── FRemnantItemState (state module)                      │
│              State = Sealed                                    │
│              EchoMods[] — hidden, inactive                     │
│              ActiveDesire — progress tracked                   │
│                                                                │
│   URemnantTrackerComponent (on AEternalPlayer)                 │
│      Subscribes to kill events, ticks Desire progress          │
│      On Desire complete → AwakenItem:                          │
│          State = Awakened                                      │
│          Each EchoMod.bIsRevealed = true, bIsActive = true     │
│          UEquipmentComponent refresh → Echo GEs spawn          │
└────────────────────────────────────────────────────────────────┘

Key Design Principles

Principle Implementation
Server authority All public subsystem + tracker methods require HasAuthority()
Reuse, don't fork Items ride UItemObject, modifiers ride FRolledModifier (with Source=Echo), equipment apply loop runs the normal FEquipmentFragment path
Marker fragment, rich state module FRemnantFragment has no fields — identifies the item. FRemnantItemState carries all mutable per-instance data
Transition-manager integrated Realm entry uses the same freeze/fade/client-ready pipeline as hub/dungeon transitions via ELevelTransitionType::InWorldTeleport
No level unload The realm is a sub-level streamed into the current world. Player doesn't leave the persistent world.
Realm brings its own light Sharing the persistent world means sharing its single active lighting sublevel, so the realm's sky/sun/fog rides FTransitionRequest.LightingConfig — swapped behind the fade, host lighting cached and restored on exit or disconnect

Core Concepts

Why Remnants Exist

The GDD positions Remnants as "physical manifestations of the world's repeating cycle" carrying "powerful, lore-driven properties that cannot be found on standard gear." They supply:

  • A bounded challenge loop (Hold-the-Ground is the MVP variant, more event types deferred)
  • A long-tail progression hook (Desire ticks while playing normally, not during the realm)
  • A reveal moment (Sealed → Awakened pays off hours of play with an instant visible change)
  • A PoE-adjacent mechanic layer that compounds with the planned Corruption system

The mechanic itself is theme-agnostic — one Era tag and one item pool were enough to validate the full loop before the narrative + art pass lands.

Portals are no longer debug placements: they now arrive procedurally, as Room.RemnantRealm rooms injected into a dungeon's topology by chance (see Dungeon System, with a per-world-map-node chance override). The Forest domain ships the first full content set — RD_Forest_Remnant room data, L_Room_Forest_Remnant room level, and the LS_Factory lighting sublevel the portal swaps in as the realm's own environment. Spawn frequency tuning and era-color visuals remain deferred.

State Module Lifecycle

A Remnant lives in two states (ERemnantState):

State Echo Mods Desire Tooltip Equipment Apply Loop
Sealed bIsRevealed=false, bIsActive=false Progress ticks while equipped ??? for Echo Mod lines; shows Desire progress X / Y Skips Echo Mods (gate via bIsActive); other mods apply normally
Awakened bIsRevealed=true, bIsActive=true No longer relevant (MVP: one Desire per item) Full Echo Mod text; Desire line removed Includes Echo Mods (gate passes); full GE spawn

Era Burdens (shipped alongside Echo Mods) follow the same reveal discipline: each rolled Remnant carries BurdenSlotCount (MVP: 1) negative modifiers (FRemnantBurden, Source=EraBurden) picked from the pool's BurdenModPool. Sealed = hidden/inactive like Echo Mods; Awakening reveals and activates them together — the payoff moment surfaces the curse with the power. Burden pools are registered as auxiliary pools (resolvable by ID, excluded from loot rolls), same as Echo pools. See RemnantEraBurdens.plan.md for design rationale.

Transition is one-shot and one-way for MVP. Multi-rank Desire chains are deferred.

Fragment vs State Module

Aspect FRemnantFragment FRemnantItemState
Where it lives UItemObject::ItemManifest (static template) UItemObject::StateModules (runtime instance)
Purpose Marker — identifies the item as a Remnant Carries mutable per-instance state
Fields None — empty marker State, EchoMods, Burdens, ActiveDesire, Era, PoolRef
Replication Via manifest replication Via ReplicatedUsing=OnRep_StateModules
Persistence Rides with manifest (ItemID lookup) Serialized via wrapper entry in FItemSaveState.StateModules

See Item State Modules for the general pattern and how future mechanics (Corruption, etc.) will layer on the same system.

Server Authority Boundary

Operation Server Client
Portal interact → subsystem Enter ✅ via UInteractableComponent server RPC
Realm sub-level stream Clients see OnLevelShown via replication
Event progress tick Reads replicated ProgressSeconds
Wave triggers Wave state replicates
Reward roll Receives item via replicated container
Desire tick on kill Reads via OnRep_StateModules
Awaken transition Receives state change via OnRep_StateModules + tooltip VM re-snapshot

The Remnant Loop

  Interact with portal (server)
  Subsystem caches entry transform
  Build FTransitionRequest (InWorldTeleport)
  UTransitionStateManager freezes everyone + fades
  ULevelStreamingDynamic loads realm sub-level
  Player teleported to realm spawn point
  Interact with event actor (server)
  Progress timer ticks while in-radius + wave spawner fires
         ├── Player dies  → ExitRealm(success=false)
         └── Timer full   → RollRemnantItem + drop loot container
                          → Closing phase (player picks up)
                          → ExitRealm(success=true)
  Subsystem teleports player to cached entry transform
  Sub-level unloads
  Player equips sealed Remnant
  URemnantTrackerComponent ticks Desire on every credited kill
  Desire satisfied → AwakenItem
         ├── State = Awakened
         ├── EchoMod[i].bIsRevealed = bIsActive = true
         └── UEquipmentComponent refresh → Echo GEs spawn

Portal & Realm Entry

ARemnantPortalActor

World-placed actor carrying an UInteractableComponent. On interact, calls the subsystem with the player, the portal's actor transform (the return destination), and the RealmLevel soft reference.

Property Purpose
RealmLevel TSoftObjectPtr<UWorld> — the sub-level streamed on enter
RealmSpawnLocation World offset where the realm level instance is placed. Zero (default) resolves to the subsystem's reserved offset, outside any generated dungeon's extent — set it only to override for hand-built test levels
RealmLighting FEnvironmentLightingConfig — the realm's own sky/sun/fog/post sublevel, swapped in behind the fade and restored on exit. Leave unset to keep the host's lighting

One-shot — the actor destroys itself after first interact.

The portal carries no era; the realm's event actor owns it (see Era Provenance).

Realm-Owned Environment Lighting

The realm is a sub-level of the persistent world, not a separate map, so it cannot bring its own sky along — the world has exactly one active lighting sublevel. RealmLighting therefore rides the transition rather than the level:

EnterRealm
    ├── RealmLighting.IsValid()?
    │       ├── PreRealmLighting = TSM->GetLightingConfig()   (cache the host's)
    │       └── Request.LightingConfig = RealmLighting
    └── StartTransition → existing RequestLoadLighting multicast swaps it behind the fade

ExitRealm
    └── Request.LightingConfig = PreRealmLighting  → host lighting restored, cache cleared

HandlePlayerDestroyed (disconnect)
    └── the disconnecting player never runs the exit transition, so the cleanup path
        calls RequestLoadLighting(PreRealmLighting) directly

No new replication: the swap reuses the transition manager's existing lighting multicast, and the fade hides the sublevel churn.

URemnantRealmSubsystem

A UWorldSubsystem that orchestrates enter/exit. Owns only realm-specific state; delegates freeze/fade/client-sync to the transition manager.

EnterRealm(Player, EntryTransform, RealmLevel, SpawnLocation)
    ├── HasAuthority check
    ├── Cache entry transform in per-player map
    ├── Bind OnDestroyed for disconnect cleanup
    ├── Subscribe to TransitionStateManager (idempotent)
    └── StartTransition(FTransitionRequest{ InWorldTeleport, RealmLevel, SpawnLocation })

HandleSublevelLoadRequest (TSM callback)
    ├── Forward direction → ULevelStreamingDynamic::LoadLevelInstance
    │                       → TSM->NotifyDynamicSublevelLoaded(level) before
    │                         signalling local streaming complete
    └── Exit direction → immediately signal streaming complete (no load work)

ExitRealm(Player, bSuccess)
    ├── HasAuthority check
    ├── Flag bExitPending
    ├── StartTransition (return direction — uses cached transform)
    └── HandleTransitionComplete unloads the sub-level + clears the cache entry

The subsystem does not own the transition UI, freeze multicast, or client-ready tracking — those live in UTransitionStateManager. See Transition Manager Integration (pending doc) for the shared pipeline.

Spawn Routing into the Streamed Realm

A realm run is an InWorldTeleport transition — the player never leaves the persistent world, so the landing PlayerStart must be resolved from the streamed realm sub-level, not the hub/fixed level the player came from. The pipeline tracks this explicitly:

RemnantRealmSubsystem loads sub-level
    └── TSM->NotifyDynamicSublevelLoaded(level)   ← TSM records the active transition's sublevel
PlayerSpawnManager (forward InWorldTeleport entry):
    ├── Context = EPlayerStartContext::InWorldTeleportPoint
    ├── Search TSM->GetActiveDynamicSublevel() EXCLUSIVELY
    │       (no fallback to WorldMapSubsystem's current streamed/hub level)
    └── No matching PlayerStart → log error (content authoring bug)

Why no hub fallback: falling back to the hub's streamed level silently teleported the player back to the hub start when entering a realm from a non-test boot path (e.g. L_Dungeon → Training Grounds → portal). The realm sub-level owns its landing PlayerStart; searching it exclusively makes a missing/mislabelled realm PlayerStart fail loudly instead of masquerading as a successful hub spawn. The exit direction uses the cached return transform and bypasses the PlayerStart pipeline entirely.


Hold-the-Ground Event

The MVP event type (GEN-01). One event actor per realm sub-level. Designer places + configures it alongside spawners.

Event Flow

 Player interacts with ARemnantEventActor_HoldGround (server)
     ├── Multicast_HideInteractable — removes prompt from all clients
     ├── bActive = true (replicated)
     ├── WaveSpawner.BeginWaves()
     └── Tick begins

 Tick (server, 60Hz):
     ├── In-radius check via squared distance
     │   ├── In  → ProgressSeconds += DeltaSeconds (clamped to target)
     │   └── Out → pause (no reset for MVP)
     ├── On ProgressSeconds >= ProgressTargetSeconds:
     │     bActive = false
     │     bIsClosing = true
     │     SpawnRemnantReward (rolls via URemnantItemFactory + spawns ALootContainer)
     │     Start ClosingSecondsRemaining countdown
     └── On ActivePlayer death (OnDeath):
           bActive = false
           URemnantRealmSubsystem::ExitRealm(Player, success=false)

 Closing phase tick:
     ClosingSecondsRemaining -= DeltaSeconds
     On zero → ExitRealm(Player, success=true)

Leash Behavior

Progress pauses on leash-break, doesn't reset. Cheapest signal to implement, tolerant of combat positioning. Reset-on-break is considered for later event types where the challenge warrants it.

Wave Spawner

ARemnantWaveSpawner holds an authored TArray<FRemnantWaveEntry> (TriggerAtSeconds + TArray<AEnemySpawner*>). Sorted at BeginPlay. Ticks elapsed seconds and fires each wave's spawners on schedule. Wraps the existing AEnemySpawner primitive — no generation coupling.

World-Space Progress Widget

Event actor owns a UWidgetComponent hosting URemnantEventProgressWidget. Drives:

  • Fill proportion from ProgressSeconds / ProgressTargetSeconds
  • Final stretch pulse when remaining time ≤ FinalStretchThresholdSeconds
  • Wave imminent flash when next wave triggers within WaveImminentWindowSeconds
  • Closing countdown once reward drops

Widget state is fed by a client-side URemnantEventViewModel populated from the event actor's replicated properties.


Item Rolling

URemnantItemFactory::RollRemnantItem(Outer, Pool)

Static factory called server-side on event success. Pool is the realm's URemnantItemPoolDataAsset.

 Pick base manifest from Pool.BaseItems (random)
 Copy manifest, run standard UItemGenerationLibrary::GenerateItemProperties
     (rolls prefix/suffix/implicit modifiers at item level — same as loot path)
 Build FRemnantItemState:
     State = Sealed
     EchoMods: pick N unique defs from Pool.EchoModPool.ModifierDefinitions
               (N = Pool.EchoModSlotCount)
               Each wrapped in FEchoMod with Source=Echo, bIsRevealed=false
     ActiveDesire: pick 1 from Pool.DesirePool, Progress=0
     Era, PoolRef: stamped from the source pool
 Item->AddStateModule<FRemnantItemState>(State)
 Return UItemObject (caller drops it via ALootContainer)

Pool Data Asset

URemnantItemPoolDataAsset is a UPrimaryDataAsset registered under PrimaryAssetType "RemnantItemPool". Holds:

Field Purpose
Era Gameplay tag for the era this pool represents
BaseItems Soft references to UItemManifestDataAsset eligible for rolling
EchoModPool Separate UModifierPoolDataAsset (kept out of loot rolls — see below)
DesirePool Authored FDesire templates
EchoModSlotCount How many Echo Mods each roll gets (MVP: 2)
BurdenModPool Era-linked negative modifiers, registered as an auxiliary pool
BurdenSlotCount How many Burdens each roll carries (MVP: 1)

Era Registry

UEraRegistryDataAsset (single global asset, PrimaryAssetType "EraRegistry") maps each Era tag to its reward pool and realm levels. Realm event actors author only their Era tag and resolve the pool through the registry (ResolveItemPool); the explicit RewardPool override remains for hand-authored cases. Save-time validation cross-checks that each entry's pool is labeled with the same era, so a portal/pool era mismatch can't ship silently. A future Domain SourceEraTag (REC-2/REC-9) resolves through the same rows.

Where Era Lives — the Era → Item Chain

Era is a property of the realm that dropped the item, never of the item's manifest. This trips up designers looking for an "era" field in the Item Manager, so state the chain plainly:

ARemnantEventActor_HoldGround.Era        (authored per realm level, on the event actor inside it)
    │  ResolveRewardPool(): explicit RewardPool override wins, else...
UEraRegistryDataAsset::ResolveItemPool(Era)
    │  FEraDefinition { EraTag, ItemPool, RealmLevels }
URemnantItemPoolDataAsset  { Era, BaseItems[], EchoModPool, BurdenModPool, DesirePool }
    │  URemnantItemFactory::RollRemnantItem picks a base from BaseItems
UItemObject  +  FRemnantItemState { State, EchoMods, Burdens, ActiveDesire, Era, PoolRef }
                                              Era stamped here, on the rolled INSTANCE

Consequences worth internalizing:

  • FRemnantFragment on the manifest is a pure marker — no fields, and specifically no era. It only says "this base is eligible to become a Remnant". URemnantItemFactory skips any pool base that lacks it.
  • Two realms of different eras can roll the same base manifest into items whose FRemnantItemState.Era differs. The manifest cannot know which.
  • The portal carries no era. Its dead Era field was deleted in the world-validation pass; the realm's event actor owns it.
  • Answering "what era does this item belong to?" for a manifest is a reverse lookup: find every era whose pool lists the manifest in BaseItems. The Item Manager does exactly this and shows the result as a read-only Drops in era row on Remnant manifests (amber when no pool lists it — that base can never drop; amber when several do — the item's era depends on where it dropped).

Echo Mod Pool Registration

Echo Mods live in their own UModifierPoolDataAsset that is intentionally excluded from the global loot lookup. UModifierPoolManager keeps three parallel pool kinds:

Pool kind Contributes to FindModifierByID? Contributes to GetFilteredModifiers (loot rolls)?
LoadedPools (standard loot)
DungeonPools
RemnantEchoPools

This means Echo Mods are resolvable by ID everywhere — tooltip rendering, modifier cache rebind on load, GE application — but cannot leak into ordinary loot rolls. Registration happens in UModifierSubsystem::LoadAndRegisterRemnantEchoPools at subsystem init by scanning PrimaryAssetType "RemnantItemPool".

Live Echo Mods

The pool ships with six Echo Mods, each a GrantedAbility proc backed by an ability base class (see Ability Classes):

Echo Mod Mechanic Ability base
echo_pressure_release Hits build pressure; nova burst at cap UPressureBuilderAbility
echo_static_charge Every 4th hit → small Electric AoE UPressureBuilderAbility (Event.StaticCharge.Released)
echo_hemorrhage_cascade Bleed stacks; burst all at 5 as Physical UStatusCascadeAbility
echo_ember_feedback When struck, 25% chance to Ignite attacker UOnDamagedAbility
echo_vital_surge Kills while ≤35% HP restore 10% MaxHealth (health-gated heal channel) UOnKillAbility (HealEffectClass)
echo_killing_fervor Each credited kill stacks a power buff (paired with a drain) UOnKillAbility (OnKillEffectClassesGA_KillingFervor, stacking GE_KillingFervor_Power + GE_KillingFervor_Drain)

UOnKillAbility carries two independent channels — the health-gated heal (Vital Surge) and the unconditional per-kill OnKillEffectClasses (Killing Fervor). The health threshold gates only the heal; it is not a heal-only base. See Ability Classes.


Desire Tracking & Awakening

URemnantTrackerComponent

Component on AEternalPlayer (player controller scope — matches inventory / compendium ownership). Lifetime spans the player's session; equipped items tick automatically.

 BeginPlay (server only):
     Subscribe to game-wide kill credit event

 HandleKillCredited(Victim):
     Compute kill facts once: bVictimIsBleeding, WearerHealthFraction
     For each UItemObject in equipment slots:
         if has FRemnantItemState and state == Sealed:
             if DoesKillSatisfyDesire(ActiveDesire.TaskTag, facts):
                 ActiveDesire.Progress += 1 (capped at Target)
                 MarkStateModulesDirty (forces replication)
                 if Progress >= Target: AwakenItem(Item)

 AwakenItem(Item):
     State = Awakened
     For each EchoMod:
         bIsRevealed = true
         bIsActive = true
     UEquipmentComponent::RefreshItemModifiers(Item)
         → equipment removes + re-runs apply loop for that item
         → Echo GEs now pass the source gate and spawn
     MarkStateModulesDirty

Kill Attribution & Desire Routing

Kills credited through the GAS damage pipeline broadcast Event.Combat.KillCredited to every contributor's pawn. The tracker computes per-kill facts once (victim bleeding? wearer health fraction?) and tests each equipped sealed Remnant's Desire via DoesKillSatisfyDesire(TaskTag, bVictimIsBleeding, WearerHealthFraction):

FDesire.TaskTag Counts the kill when
Desire.Kill.Any (or empty) Always (back-compat fallback)
Unknown tag Always, but logs an error once per tag (authoring drift must not be silent)
Desire.Kill.Bleeding Victim was Bleeding
Desire.Kill.LowHealth Wearer was below 50% Health (LowHealthDesireThreshold)

Environmental / hazard kills that bypass GAS (world damage volumes, fall damage) do not credit. Attribution through those paths is deferred — would require a world-damage → GAS bridge or a parallel credit event.


Echo Mod Reveal Gate

Echo Mods are standard FRolledModifier structs with Source = ECraftingModifierSource::Echo. They're stored inside FRemnantItemState::EchoMods (each wrapped in an FEchoMod adding bIsRevealed and bIsActive flags), not in the item's FEquipmentFragment::Modifiers array.

Apply-Time Gate

The equipment apply loop — the same one that handles implicit / prefix / suffix / scaling mods on every equipped item — runs a ShouldApply(Item, Mod) check before spawning a GE:

Modifier Source Gate
Drop (standard loot roll) Always true
Crafted (crafting station output) Always true
Echo (Remnant) True iff FEchoMod::bIsActive on the owning item's state module

Sealed Remnants equip normally; only their prefix/suffix/implicit mods apply. On awaken, bIsActive flips and UEquipmentComponent::RefreshItemModifiers(Item) re-runs the apply loop for that item, spawning Echo GEs as regular equipment-owned handles. No dual-ownership — equipment is the sole GE handle owner for all modifier sources.

Tooltip Reveal Gate

FModifierDefinition::FormatDescription(Value, bRevealValue) returns a placeholder (???) when bRevealValue = false. The tooltip ViewModel reads per-Echo-Mod bIsRevealed from the state module and passes it to the formatter. Non-Echo mods always reveal.

Desire line (Kill any enemy X / Y) renders on the tooltip while the item is Sealed, suppressed after awaken.


Replication Strategy

What Replicates

Property Where Replication
UItemObject::StateModules UItemObject ReplicatedUsing=OnRep_StateModules, broadcasts OnItemStateModulesChanged
Event ProgressSeconds, bActive, bIsClosing, ClosingSecondsRemaining ARemnantEventActor_HoldGround UPROPERTY(Replicated) — clients read for widget state
Wave ElapsedSeconds, NextWaveIndex, bActive ARemnantWaveSpawner UPROPERTY(Replicated) — clients use for UI countdowns

Sub-Struct Mutation Gotcha

TArray<TInstancedStruct<>> property mutations inside a contained struct (e.g. flipping FEchoMod::bIsRevealed) don't flag the parent array dirty automatically. Server callers mutate via GetStateModuleMutable<T>() and then call UItemObject::MarkStateModulesDirty() to force a net update. Listen-server host also broadcasts OnItemStateModulesChanged locally since it won't receive its own OnRep.

Client-Side Reaction

Tooltip ViewModels, equipment UI, and any other consumer of Remnant state subscribe to UItemObject::OnItemStateModulesChanged. On fire, they re-snapshot from the state module — no property-by-property change callbacks.

Multiplayer Scope

MVP is solo. All code paths are server-authoritative (so coop-safe in principle), but the edge cases (partial-death event exit policy, per-player leash radius, shared entry transforms for party entry) are deferred until coop rules are locked.


Public Contracts

URemnantRealmSubsystem (WorldSubsystem)

Method Parameters Purpose
EnterRealm (Player, EntryTransform, RealmLevel, SpawnLocation) Begin a realm run. Server-only.
ExitRealm (Player, bSuccess) End a realm run, teleport player back. Server-only.
IsPlayerInRealm (Player) Query whether a player is currently inside a realm.
GetActiveRealmLevel Returns the streamed ULevel* while a realm is active, else nullptr. Used by GameState so items dropped during a realm run are owned by the realm level.

ARemnantPortalActor

Property Purpose
Era Gameplay tag passed through to the rolled item
RealmLevel Soft reference to the sub-level streamed on interact
RealmSpawnLocation Landing point inside the realm

ARemnantEventActor_HoldGround

Property Purpose
LeashRadius In-radius threshold for progress tick
ProgressTargetSeconds Event completion time
WaveSpawner Placed spawner reference
RewardPool Pool used for RollRemnantItem on success
ClosingDurationSeconds Time the realm stays open after reward drop

URemnantTrackerComponent

Lifecycle-driven — subscribes to kill events in BeginPlay, ticks equipped items internally. One pure helper is public/static for unit testing:

Member Purpose
DoesKillSatisfyDesire(TaskTag, bVictimIsBleeding, WearerHealthFraction) Static. Routes a kill to a Desire's TaskTag (Any / Bleeding / LowHealth)
LowHealthDesireThreshold Constant 0.5f — wearer health fraction for Desire.Kill.LowHealth

URemnantItemFactory

Method Parameters Purpose
RollRemnantItem (Outer, Pool) Static. Rolls a full Sealed Remnant with Echo Mods + Desire.

Data Types

Struct / Enum Purpose
ERemnantState Sealed / Awakened
FEchoMod Rolled modifier + bIsRevealed + bIsActive gates
FDesire TaskTag, Progress, Target, DescriptionTemplate
FRemnantItemState ERemnantState, EchoMods, ActiveDesire, Era, PoolRef — attached as state module
FRemnantFragment Empty marker fragment in item manifest
URemnantItemPoolDataAsset Primary data asset: base items, Echo Mod pool, Desire pool

Source References

Component Location
URemnantRealmSubsystem Public/Remnant/Subsystems/RemnantRealmSubsystem.h
URemnantTrackerComponent Public/Remnant/Components/RemnantTrackerComponent.h
URemnantItemFactory Public/Remnant/Factory/RemnantItemFactory.h
ARemnantPortalActor Public/Remnant/Actors/RemnantPortalActor.h
ARemnantEventActor_HoldGround Public/Remnant/Actors/RemnantEventActor_HoldGround.h
ARemnantWaveSpawner Public/Remnant/Actors/RemnantWaveSpawner.h
FRemnantFragment Public/Remnant/Fragments/RemnantFragment.h
FRemnantItemState, FEchoMod, FDesire Public/Remnant/Types/RemnantTypes.h
URemnantItemPoolDataAsset Public/Remnant/Data/RemnantItemPoolDataAsset.h
Echo pool registration Private/Inventory/SubSystems/ModifierSubsystem.cppLoadAndRegisterRemnantEchoPools
Echo pool storage Public/Inventory/Modifiers/ModifierPoolManager.hRemnantEchoPools
Sublevel tracking for spawn routing Public/GameMode/Components/TransitionStateManager.hNotifyDynamicSublevelLoaded() / GetActiveDynamicSublevel()
InWorldTeleport PlayerStart resolution Private/GameMode/Components/PlayerSpawnManager.cpp
Log category Public/Remnant/RemnantLog.h


Recent Changes

Date Change Impact
2026-08-06 Procedural portals + realm-owned lighting Portals arrive as chance-injected Room.RemnantRealm dungeon rooms (Forest ships RD_Forest_Remnant / L_Room_Forest_Remnant / LS_Factory); ARemnantPortalActor::RealmLighting swaps the world's lighting sublevel behind the transition fade, cached as PreRealmLighting and restored on exit and on disconnect cleanup. RealmSpawnLocation documented as a world offset — zero uses the subsystem's reserved offset, so procedural portals need no authored position. Dead Era property row removed
2026-07-08 Era → item chain documented; era provenance surfaced in the Item Manager Era belongs to the realm's reward pool, never to the manifest. A Remnant manifest's details panel now shows a read-only Drops in era row derived by reverse lookup through DA_EraRegistry, and flags bases no pool lists
2026-07-07 Killing Fervor Echo Mod (6th live mod) echo_killing_fervorGA_KillingFervor, a UOnKillAbility using the new unconditional OnKillEffectClasses channel (stacking GE_KillingFervor_Power + GE_KillingFervor_Drain); the base class's health threshold now gates only the heal channel, not the whole ability
2026-06-11 Echo pool filled (5 live mods) + Desire TaskTag routing Pressure / Static Charge / Hemorrhage Cascade / Ember Feedback / Vital Surge Echo Mods backed by new proc ability bases; DoesKillSatisfyDesire routes kills to Desire.Kill.Any/Bleeding/LowHealth; new Event.Combat.KillCredited / DamageReceived events
2026-04 Remnant persistence via state module wrapper FRemnantItemState round-trips through save/load; equipped awakened Remnants survive relog
2026-04 Echo pool registered with UModifierPoolManager (3rd pool kind) Echo Mod IDs resolve everywhere via FindModifierByID; sealed from loot rolls
2026-04 Era + PoolRef moved from FRemnantFragment to FRemnantItemState Fragment is now a pure marker; origin identity persists with the instance
2026-04 Realm teleport routed through UTransitionStateManager::InWorldTeleport Unified freeze/fade/client-ready with hub/dungeon transitions; coop-ready machinery
2026-04-24 InWorldTeleport spawns routed to the streamed realm sub-level Realm PlayerStart resolves from the loaded sub-level exclusively (no hub fallback); fixes wrong-spawn from non-test boot paths and surfaces missing realm PlayerStarts as a loud authoring error
2026-04-21 Initial MVP Portal → realm → Hold-the-Ground → sealed item → equip → auto-awaken loop