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)

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

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
    └─ Multicast_SetInteractText(ItemName)

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

Player approaches AItemActor
    ├─ Interaction prompt shown
Player interacts
    ├─ Server_TryAddItem(ItemComponent)
    ├─ [Server] Manifest item → add to inventory
    ├─ [Success?] ItemComponent->PickedUp() → destroy actor
    └─ [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 Displays item name
InteractText Display name (replicated)
Method Purpose
GetItemComponent() Access item data
Multicast_SetInteractText(Text) Sync name to clients

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

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 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