Skip to content

Loot System

Summary: Data-driven loot generation using weighted random selection from ULootTableDataAsset. Items are filtered by context (level, tags), generated with appropriate properties, and spawned as interactable AItemActor pickups. All generation is server-authoritative.

Table of Contents


Architecture Overview

ULootTableDataAsset (Design-Time)
│
├── FLootTableEntry[] (Item Drops)
│   ├── UItemManifestDataAsset
│   ├── DropWeight (probability distribution)
│   ├── DropChance (0.0 - 1.0)
│   ├── LevelOffsetRange
│   └── Context Filters (tags, level range)
│
├── FCurrencyDropEntry[] (Currency)
│   ├── StackRange
│   └── DropWeight, DropChance
│
└── ParentTables[] (Inheritance)

ULootComponent (Runtime - on enemies/containers)
│
├── LootTable reference
├── SourceContextTags
└── QuantityMultiplier
    │
    ▼
ULootGenerator::GenerateLoot(LootTable, Context)
    │
    ├── Filter valid entries (level, tags)
    ├── Weighted random selection
    ├── Item level calculation
    └── FLootGenerationResult
        │
        ▼
UItemSpawner::SpawnNewItemAtLocation()
    │
    └── AItemActor (World Pickup)
        ├── UItemComponent
        └── UItemDropWidget

Key Design Principles

Principle Implementation
Data-Driven Loot tables as DataAssets, no hardcoded drops
Weighted Random Drop weights create probability distribution
Context Filtering Tags and level ranges filter valid entries
Server-Authoritative All generation on server only
World Pickups Items spawn as interactable actors

Core Concepts

Why Data-Driven?

Hardcoded loot: - Requires recompilation for balance changes - No designer autonomy - Difficult A/B testing

Data-driven loot: - Designers edit DataAssets directly - Hot-reloadable in editor - Easy to create themed loot tables (boss, treasure, trash)

Two-Phase Drop Chance

Each item has two chances to fail:

Roll 1: Base Drop Chance (LootTable.BaseDropChance)
    │
    ├─ [Fail] → No item this roll
    │
    └─ [Pass] → Select weighted entry
           │
           ▼
Roll 2: Entry Drop Chance (Entry.DropChance)
    │
    ├─ [Fail] → No item this roll
    │
    └─ [Pass] → Generate item

This allows: - Base chance: "Does anything drop?" - Entry chance: "If something drops, does THIS specific item drop?"

Guaranteed vs Random Items

Type Use Case
LootTable drops Random selection from pool
GuaranteedItemManifest Always drops (quest items, boss loot)

Which sources fire is gated per component by ELootDropMode (LootTableOnly, GuaranteedOnly, Both) — a container can be a pure treasure chest, a pure quest-item holder, or both at once.

Naming caveat: the Currency* names (FCurrencyDropEntry, CurrencyEntries, GeneratedCurrency) are legacy struct naming. The currency concept was removed from the item taxonomy — crafting materials are Cores and Fragments (GameItems.Types.Core / GameItems.Types.Fragment); these entries drop stackable material items, not money.


Loot Tables

ULootTableDataAsset Structure

LootTable: DA_Enemy_Skeleton
├── TableID: "skeleton_common"
├── BaseDropCountRange: (1, 3)
├── BaseDropChance: 0.8
│
├── ItemEntries:
│   ├── [0] Bone Shard    (Weight: 100, Chance: 1.0)
│   ├── [1] Rusty Sword   (Weight: 20,  Chance: 0.8)
│   └── [2] Gold Ring     (Weight: 5,   Chance: 0.5)
│
├── CurrencyEntries:
│   └── [0] Gold (Weight: 100, StackRange: 5-25)
│
└── ParentTables:
    └── DA_Common_Drops

FLootTableEntry Properties

Property Purpose
ItemManifest The item to drop
DropWeight Relative probability (higher = more common)
DropChance Additional roll after selection
ItemLevelOffsetRange Level variance from source
RequiredContextTags Must have these tags to appear
ForbiddenContextTags Cannot appear with these tags
MinSourceLevel Minimum source level
MaxSourceLevel Maximum source level (0 = no limit)
RequiredPlayerQuestTags Eligible only while some connected player holds one of these quest tags (quest items start dropping after a beat)
ForbiddenPlayerQuestTags Skipped when any connected player holds one of these (quest items stop dropping once found / turned in)

Quest-tag filters are deliberately any-player, not killer-attributed: loot is a shared world event and enemy death drops carry no instigator. They need FLootSourceContext::WorldContextObject (set by ULootComponent::BuildLootContext) to reach UPlayerQuestComponent::AnyPlayerHasQuestTag; without a world context the entry fails closed.

Table Inheritance

Parent tables allow shared drop pools:

DA_Common_Drops (Parent)
├── Health Potion
└── Mana Potion

DA_Enemy_Skeleton
├── ParentTables: [DA_Common_Drops]
├── Bone Shard
└── Rusty Sword

Result: Skeleton can drop Health/Mana Potions + Bone/Sword

FLootSourceContext

Context passed to generation:

Property Purpose
SourceLevel Level of drop source (enemy/chest)
QuantityModifier Multiplier for drop count
ContextTags Tags for filtering (e.g., "boss", "elite")
bGuaranteedDrop Skip base drop chance
InstigatingPlayer Player who triggered drop
WorldContextObject World access for the player-quest-tag filters

Generation Algorithm

Selection Process

GenerateLoot(LootTable, Context)
    │
    ├─ 1. Collect all valid entries
    │   └─ Include parent table entries
    │
    ├─ 2. Filter by context
    │   ├─ Level within MinSourceLevel..MaxSourceLevel
    │   ├─ RequiredContextTags present
    │   └─ ForbiddenContextTags absent
    │
    ├─ 3. Calculate drop count
    │   ├─ BaseCount = RNG(BaseDropCountRange.X, BaseDropCountRange.Y)
    │   └─ FinalCount = BaseCount * Context.QuantityModifier
    │
    ├─ 4. For each drop roll:
    │   ├─ Check base drop chance (skip if guaranteed)
    │   ├─ Select weighted random entry
    │   ├─ Check entry drop chance
    │   ├─ Calculate item level
    │   └─ Add to result
    │
    └─ 5. Generate currency drops separately

Weighted Selection

Entries with weights: [Bone: 100, Sword: 20, Ring: 5]
Total weight: 125

Roll random 0-124:
├── 0-99   → Bone    (80% chance)
├── 100-119 → Sword  (16% chance)
└── 120-124 → Ring   (4% chance)

Item Level Calculation

SourceLevel: 10
Entry.ItemLevelOffsetRange: (-2, +2)

ItemLevel = SourceLevel + RNG(-2, +2)
ItemLevel = Max(1, ItemLevel)  ← Clamp to minimum 1

Possible levels: 8, 9, 10, 11, 12

World Pickups

AItemActor Purpose

  • Represents item in world
  • Displays item name widget
  • Handles player interaction
  • Replicates to all clients

Spawn Flow

FItemManifest::SpawnPickupActor(World, Location, Rotation)
    │
    ├─ Spawn AItemActor from PickupActorClass
    │
    ├─ Get UItemComponent from actor
    │
    ├─ InitItemManifest(Manifest) on component
    │
    └─ SetInteractText(ItemName)  (replicated; OnRep wires the nameplate, late joiners included)

UItemSpawner Batching

Multiple items spawn with spacing to prevent overlap:

SpawnLootInWorld(LootResult)
    │
    ├─ Create UItemSpawner
    │
    ├─ For each item:
    │   ├─ FindValidSpawnLocation(BaseLocation, Radius, ExistingLocations, Spacing)
    │   ├─ Add to ExistingLocations
    │   └─ SpawnPickupActor at valid location
    │
    └─ Destroy spawner

Pickup Interaction

World items carry a PickupSphere (AItemActor::PickupRadius, 250 uu). Overlapping local players register the item with their ULootFocusComponent (on the AEternalPlayer PlayerController), which owns the client-side notion of focus. It re-evaluates on a 0.1 s poll (nothing else re-triggers as the pawn walks a pile); the right-stick browse cone is CycleConeHalfAngle = 60°, and the pure pickers PickNearestWithHysteresis / PickInScreenDirection are static so specs can drive them. Focus is pad-gated by IsLocalGamepadPlayer().

Device Focus rule Pickup verb
Mouse hovered nameplate (no hover = no focus; plates look as before) click the nameplate, or Interact
Gamepad nearest item in range, 40 uu hysteresis; right-stick flick (no lock) browses in that direction, sticky until the item goes or the pawn walks 150 uu Interact (d-pad up); plate prefixes the key while focused

The focused item's tooltip docks bottom-right (ULootTooltipDockWidget, hosted by UHUDController).

Nameplates and stacking

AItemActor::ItemWidgetComponent is a UItemNameplateComponent (screen-space UWidgetComponent, bDrawAtDesiredSize, pivot (0.5, 1) so the plate stands on the item's ground point). The pad glyph + gold border show on gamepad only; mouse hover keeps the authored button tint. Name text joins CommonUI text scaling.

Piles would overprint, so ULootLabelLayoutSubsystem (client-local UTickableWorldSubsystem; never created on a dedicated server, Game/PIE worlds only) projects every registered nameplate each frame and ResolveOverlaps() pushes overlapping plates upward only — the plate lowest on screen keeps its spot, the ones behind climb, so a pile reads as a column. Purely screen-space: items and widgets never move. Nameplates self-register in OnRegister/OnUnregister and apply the resolved pixel offset via ModifyProjectedLocalPosition.

CVar Default Purpose
Eternal.Loot.LabelGap 2 Vertical gap (viewport units) between stacked plates
Player in PickupRadius / hovering plate
    │
    ├─ ULootFocusComponent picks focus → nameplate glyph (pad) + corner tooltip
    │
    ▼
Click plate / Interact key (loot focus wins over NPC interactables;
    │                          `TryInteract` is a no-op while any gameplay panel is open — client courtesy behind the input gate)
    │
    ├─ Server_TryAddItem(ItemComponent)
    │
    ├─ [Server] reject if pawn outside PickupRadius + slack
    │
    ├─ [Server] Manifest item → add to inventory
    │
    ├─ [Success?] ItemComponent->PickedUp() → destroy actor → next nearest item focuses
    │
    └─ [Fail?] Item remains in world

LootComponent Integration

Purpose

ULootComponent attaches to actors that drop loot: - Enemies (drop on death) - Chests (drop on open) - Destructibles (drop on break)

Configuration

Property Purpose
DropMode ELootDropMode: which sources fire (table / guaranteed / both)
LootTable Primary drop table
LevelOverride Override source level (0 = use actor level)
SourceContextTags Additional context tags
QuantityMultiplier Scale drop count
bGuaranteedDrop Always drop something
GuaranteedItemManifest Always drops this specific item

Enemy Death Integration

AEternalEnemy::Multicast_HandleDeath()
    │
    ├─ [HasAuthority()?]
    │   └─ LootComponent->DropLoot(InstigatingController)
    │
    └─ SetLifeSpan(5.f)  ← Corpse cleanup

DropLoot Flow

DropLoot(InstigatingController)
    │
    ├─ Build FLootSourceContext
    │   ├─ SourceLevel = GetSourceLevel()
    │   ├─ ContextTags = SourceContextTags
    │   └─ InstigatingPlayer = InstigatingController
    │
    ├─ [LootTable?]
    │   ├─ GenerateLoot(LootTable, Context)
    │   └─ SpawnLootInWorld(Result)
    │
    └─ [GuaranteedItemManifest?]
        └─ SpawnGuaranteedItem()

Public Contracts

ULootTableDataAsset

Property Purpose
TableID Unique identifier
TableName Display name
BaseDropCountRange Min/max drops per roll
BaseDropChance Probability of any drop
ItemEntries Item drop definitions
CurrencyEntries Currency drop definitions
ParentTables Inherited loot tables
Method Purpose
GetAllEntries() All entries including parents
GetValidEntries(Context) Filtered by context
GetTotalWeight(Entries) Sum of weights

ULootGenerator

Method Parameters Returns
GenerateLoot (LootTable, Context, Seed) FLootGenerationResult

FLootGenerationResult

Property Purpose
GeneratedItems Array of generated FItemManifest
GeneratedCurrency Array of currency FItemManifest
TotalRolls Number of drop attempts
bSuccess Generation completed

ULootComponent

Property Purpose
LootTable Drop table reference
LevelOverride Override source level
SourceContextTags Context tag container
QuantityMultiplier Drop count multiplier
bGuaranteedDrop Skip base chance
GuaranteedItemManifest Always-drop item
Method Purpose
DropLoot(Controller) Trigger loot generation and spawn

UItemSpawner

Method Purpose
SpawnNewItem(Manifest, Count, Controller) Spawn near player
SpawnNewItemAtLocation(Manifest, Count, Location) Spawn at position
SpawnExistingItem(Manifest, Count, Controller) Spawn dropped item

AItemActor

Property Purpose
ItemComponent Holds item manifest
ItemWidgetComponent UItemNameplateComponent — screen-space nameplate, self-registers for stacking
PickupSphere / PickupRadius USphereComponent, 250 uu; registers overlapping local players' loot focus; server re-checks on pickup
InteractText Display name (replicated)
Method Purpose
GetItemComponent() Access item data
SetLootFocused(bool) Client-side focus cue on the nameplate
SetInteractText(Text) Server-only; InteractText is ReplicatedUsing, OnRep wires the nameplate on every client (late joiners included)

Source Reference

Core Classes

File Location Purpose
LootTableDataAsset.h Loot/LootTableDataAsset.h Drop table definition
LootGenerator.h Loot/LootGenerator.h Generation algorithm
LootComponent.h Loot/Components/LootComponent.h Actor attachment
ItemSpawner.h Inventory/Items/ItemSpawner.h World spawning
ItemActor.h Inventory/Items/ItemActor.h World pickup actor
ItemComponent.h Inventory/Items/ItemComponent.h Item data on actor
ItemNameplateComponent.h Inventory/Items/Components/ItemNameplateComponent.h Screen-space nameplate widget component
LootFocusComponent.h Loot/Components/LootFocusComponent.h Client-side pickup focus (PlayerController)
LootLabelLayoutSubsystem.h Loot/Subsystems/LootLabelLayoutSubsystem.h Nameplate de-overlap (client-only world subsystem)

Key Functions

Function File Purpose
GenerateLoot() LootGenerator.cpp Main generation
SelectWeightedEntry() LootGenerator.cpp Weighted selection
GetValidEntries() LootTableDataAsset.cpp Context filtering
DropLoot() LootComponent.cpp Trigger drops
SpawnLootInWorld() LootComponent.cpp Batch spawning
SpawnPickupActor() ItemManifest.cpp Single item spawn


Recent Changes

Date Change Impact
2026-08-20 Nameplate stacking + quest-tag loot gating documented UItemNameplateComponent + ULootLabelLayoutSubsystem (Eternal.Loot.LabelGap); Required/ForbiddenPlayerQuestTags any-player filter; AItemActor pickup sphere / focus API
2026-08 FItemManifest.bNotifyOnPickup Notable items drive a persistent unseen-item badge on the inventory button
2026-07 Era registry reward pools UEraRegistryDataAsset maps Era tag → Remnant reward pool; HoldGround events resolve drops through it
2026-07 Currency taxonomy removed Material drops are Cores/Fragments; Currency* struct names are legacy
2026-03 ELootDropMode Per-container gating of table vs guaranteed drops
- Initial weighted loot system Data-driven drop tables
- Context filtering Level and tag-based entry filtering
- Currency drops Separate currency generation
- Table inheritance Shared drop pools via ParentTables
- Guaranteed items Always-drop items alongside random