Files
ashen-realms/docs/playable-slices/Ashen Realms – Playable Slice 0.4_ First Loot.md
Bastian Wagner 5b662aad4c docs
2026-08-19 11:14:17 +02:00

22 KiB
Raw Blame History

Ashen Realms Playable Slice 0.4: First Loot

Status: Ready for implementation
Prerequisite: Playable Slice 0.3 First Combat
Scope: First server-authoritative reward and loot flow
Primary Enemies: Aschenratte, Straßenräuber
Next Slice: Playable Slice 0.5 First Upgrade


1. Goal

Playable Slice 0.4 extends the existing gameplay loop:

World
→ Travel
→ Hunt
→ Encounter
→ Combat
→ Victory

to:

World
→ Travel
→ Hunt
→ Encounter
→ Combat
→ Victory
→ XP
→ Silver
→ Loot
→ Reward Summary

This slice must prove that:

  • rewards are generated only by the backend
  • rewards originate only from a valid won combat
  • each combat can grant rewards at most once
  • XP and silver are persisted
  • loot is rolled server-side
  • item rewards are persisted
  • loot generation is deterministic in tests
  • the frontend only presents server-generated rewards
  • refreshing the result screen does not reroll loot

This slice deliberately stops before equipment management.

The next slice will implement:

Playable Slice 0.5  First Upgrade

Loot
→ Inventory
→ compare item
→ equip item
→ effective stats increase

2. Core player flow

The intended flow is:

Verbrannte Straße
↓
Jagd beginnen
↓
choose encounter
↓
combat
↓
monster reaches 0 HP
↓
Combat = WON
↓
server resolves rewards
↓
XP granted
↓
silver granted
↓
loot table rolled
↓
reward persisted
↓
frontend shows reward summary

Example:

Sieg

Aschenratte besiegt

+8 XP
+6 Silber

Beute:
Aschenfell

or:

Sieg

Straßenräuber besiegt

+16 XP
+12 Silber

Beute:
Räuberklinge

3. Scope

Implement:

persistent character XP
persistent character silver

ItemDefinition where needed
CharacterItem minimal inventory persistence

LootTable
LootTableEntry

LootService
Reward/CombatReward persistence

server-side silver roll
server-side loot roll

reward resolution after won combat

reward idempotency

reward summary API data

victory reward presentation

4. Explicit non-goals

Do not implement:

equipment
equip/unequip actions
effective equipment stats
item comparison UI
full inventory management
24-slot inventory restrictions
selling items
merchants
area currencies
sets
boss guaranteed loot
pity systems
crafting
item upgrades
random affixes
quest rewards

Slice 0.4 is about obtaining rewards.

Slice 0.5 is about using equipment.


5. Reward ownership

Rewards are entirely server-authoritative.

The backend decides:

whether the combat qualifies for rewards
XP amount
silver amount
loot rolls
item definitions
item quantities
whether rewards were already granted

The frontend must never submit:

XP
silver
itemId
drop result
drop chance
loot-table result
reward quantity

as authoritative data.


6. Reward source

Rewards may only originate from a Combat with:

status = WON

A combat with:

ACTIVE

or:

LOST

must never grant normal victory rewards.


7. Reward idempotency

A won combat may grant rewards exactly once.

This is a critical invariant.

The following must never happen:

win combat
↓
receive loot
↓
refresh browser
↓
receive loot again

or:

repeat reward endpoint
↓
farm duplicate rewards from one combat

Protect this using persistent reward state and a database uniqueness constraint where practical.


8. Recommended reward persistence

Introduce a persisted reward record.

Conceptually:

CombatReward

Required fields may include:

id: uuid;

combatId: uuid;
characterId: uuid;

experienceGranted: number;
silverGranted: number;

createdAt: timestamptz;

The relation:

Combat → CombatReward

must effectively be one-to-one.

combatId should therefore be unique.

This record acts as proof that the combat has already been rewarded.


9. Character progression fields

The character needs persistent values for:

experience
silver

If experience already exists, reuse it.

If silver already exists in another persistent player-state structure, reuse that instead of adding duplicate fields.

Do not create competing currency models.

For this slice:

experience += reward XP
silver += reward silver

10. Leveling scope

XP must be persisted.

However, do not expand this slice into a full progression system unless a minimal level-up path is already present.

The existing XP curve is:

Level 1 → 2: 100 XP
Level 2 → 3: 180 XP
Level 3 → 4: 300 XP
Level 4 → 5: 450 XP
Level 5 → 6: 650 XP
Level 6 → 7: 900 XP

If the project already supports level-up calculations, use the existing implementation.

Otherwise Slice 0.4 may persist XP without implementing the complete later-level progression architecture.

The public reward contract should remain compatible with future level-up information.


11. ItemDefinition

Reuse the project's existing ItemDefinition if already implemented.

For this slice, at least the following item content is needed:

worn-short-sword
bandit-blade
bandit-hood

depending on the existing seed.

Required loot-relevant fields should already support or conceptually include:

id
key
name
description

type
equipmentSlot
rarity
tier
requiredLevel

weaponDamage
bonusHp
bonusAttack
bonusArmor

sellPrice
iconPath

Do not create a parallel item entity.


12. CharacterItem

Introduce or reuse minimal persistent player-owned item state.

Conceptually:

id: uuid;

characterId: uuid;
itemDefinitionId: uuid;

quantity: number;

createdAt: timestamptz;
updatedAt: timestamptz;

For equipment items, quantity may normally be:

1

If the existing architecture treats equipment instances separately instead of stacking them, preserve that convention.

Do not redesign the inventory model unnecessarily for this slice.


13. LootTable

Create or reuse:

LootTable

Conceptual fields:

id: uuid;

key: string;
name: string;

createdAt: timestamptz;
updatedAt: timestamptz;

Monster definitions should reference their loot table through the project's chosen data model.

Stable keys are preferred.

Example:

ash-rat-loot
road-bandit-loot

14. LootTableEntry

Create:

LootTableEntry

Conceptual fields:

id: uuid;

lootTableId: uuid;
itemDefinitionId: uuid;

dropChance: decimal;

minQuantity: number;
maxQuantity: number;

enabled: boolean;

The model should support future guaranteed drops cleanly.

Do not add complex conditional loot scripting in this slice.


15. First Aschenratte rewards

According to the existing balancing specification:

Guaranteed

47 Silver
8 XP

Material

60 % Aschenfell / Tierrest

Equipment

8 % simple starter-slot equipment

For this technical slice, the minimal useful implementation should preserve those documented probabilities where the necessary item definitions exist.

If Aschenfell has not yet been modeled as an item and introducing generic material items would unnecessarily expand scope, equipment loot may be prioritized first.

Any deliberate omission must be documented.

Do not silently invent replacement probabilities.


16. First Straßenräuber rewards

According to the balancing specification:

Guaranteed

915 Silver
16 XP

Equipment

18 % Räuberklinge
12 % Räuberhaube
8 % Plündererhandschuhe
10 % Kleiner Heiltrank

For Slice 0.4, only already-supported item types need to be persisted.

At minimum the slice should support:

Räuberklinge
Räuberhaube

if these are already part of the technical mini-slice content.

Do not implement potions or combat consumables solely because the final loot table contains them.

Unsupported entries may be deferred explicitly.


17. Loot rolls are independent

Unless the existing design specifies otherwise, separate listed drops are independent rolls.

Conceptually for Straßenräuber:

roll Räuberklinge
roll Räuberhaube

This means one combat may produce:

nothing
one item
multiple items

depending on the configured loot entries.

Do not force exactly one item drop.


18. Silver roll

Silver is guaranteed but variable.

Example for Aschenratte:

min = 4
max = 7

The backend rolls an inclusive integer:

4
5
6
or
7

For Straßenräuber:

915

The silver roll must use the same testable randomness abstraction approach already used for hunting where appropriate.

Do not make reward tests flaky.


19. Random abstraction

Reuse the existing injectable RandomSource from Slice 0.2 if available.

Do not create another independent random abstraction.

The same infrastructure should be usable for:

encounter selection
silver roll
loot rolls

Tests must be able to inject fixed random values.


20. LootService

Create or extend:

LootService

The LootService should be responsible for deterministic reward calculation from content definitions.

Conceptual API:

rollLoot(
  monsterDefinitionId: string,
): Promise<LootRollResult>

The result may contain:

{
  items: [
    {
      itemDefinitionId: string;
      quantity: number;
    }
  ]
}

Do not grant items inside the pure roll portion if that makes deterministic unit testing difficult.

Prefer separating:

roll

from:

grant/persist

where that matches existing architecture.


21. RewardService or orchestration boundary

Reward resolution should be orchestrated in one clear service boundary.

Possible name:

CombatRewardService

or equivalent existing architecture.

Responsibilities:

validate won combat
↓
ensure reward not already granted
↓
load monster reward definition
↓
determine XP
↓
roll silver
↓
roll loot
↓
persist Character XP
↓
persist Character silver
↓
persist CharacterItem rewards
↓
persist CombatReward
↓
commit
↓
return reward DTO

All persistence must happen transactionally.


22. Reward transaction

The reward operation must be atomic.

Conceptually:

lock Combat
↓
verify WON
↓
verify no CombatReward exists
↓
calculate rewards
↓
update character XP
↓
update character silver
↓
grant items
↓
create CombatReward
↓
commit

If persistence fails midway:

no partial reward should remain

23. When rewards are resolved

Preferred behavior:

When a combat action changes:

ACTIVE

to:

WON

the backend may immediately resolve rewards in the same higher-level combat completion flow.

Alternatively, a dedicated reward-resolution endpoint may be used if the project architecture strongly favors that.

However:

reward resolution must remain idempotent and server-owned.

Do not rely on the frontend calling a special endpoint exactly once for correctness.

If a separate endpoint exists, repeated calls must return the already-created result rather than grant new rewards.


24. Preferred API behavior

The cleanest player-facing result is for the final combat response to contain or reference reward information after victory.

Example:

{
  "id": "combat-uuid",
  "status": "WON",
  "round": 4,

  "player": {
    "currentHp": 82,
    "maxHp": 100
  },

  "monster": {
    "name": "Aschenratte",
    "currentHp": 0,
    "maxHp": 45
  },

  "rewards": {
    "experience": 8,
    "silver": 6,
    "items": []
  }
}

If rewards are represented as a separate resource, the API should still make retrieval straightforward.


25. Reward retrieval

A browser refresh after victory must not lose the reward summary.

Therefore:

GET /api/combats/:combatId

should either include the persisted reward or provide a stable reference to it.

Do not keep victory rewards only in transient Angular state.


26. Reward DTO

Conceptually:

interface CombatRewardDto {
  experience: number;
  silver: number;

  items: Array<{
    characterItemId: string;
    item: {
      key: string;
      name: string;
      rarity: string;
      iconPath: string;
    };
    quantity: number;
  }>;
}

Do not expose:

drop chance
random roll result
loot-table internal ID

to the normal player-facing DTO.


27. Item persistence

When an item drops, the item must exist as actual persistent player state before the response is returned.

The frontend must not merely show a cosmetic reward card.

Example:

Räuberklinge dropped
↓
CharacterItem created
↓
reward DTO references CharacterItem

Slice 0.5 will use that persisted item.


28. Duplicate items

Duplicate equipment drops are allowed.

The existing design explicitly allows duplicate items.

Do not add:

duplicate protection
salvaging
automatic conversion
pity system

in this slice.


29. No boss guarantee logic yet

The full balancing design later includes:

guaranteed boss progression item
+
special boss roll

Do not implement that system for Slice 0.4.

The initial monsters are:

Aschenratte
Straßenräuber

Keep the implementation sufficient for normal-monster loot while allowing future extension.


30. Combat completion integration

Modify the Slice 0.3 victory flow.

Previously:

Combat = WON
↓
no rewards

Now:

Combat = WON
↓
reward resolution
↓
Combat response contains persisted rewards

The combat engine itself must remain unaware of loot.

Keep this boundary:

CombatEngineService
    ↓
returns WON

CombatService / orchestration
    ↓
persists combat completion

Reward service
    ↓
grants rewards

Do not add loot logic to CombatEngineService.


31. Frontend victory screen

Replace the temporary Slice 0.3 victory placeholder with a real reward summary.

Example:

SIEG

Aschenratte wurde besiegt.

Belohnungen

+8 XP
+6 Silber

Keine besondere Beute gefunden.

[ Zur Jagd ]

If an item drops:

SIEG

Straßenräuber wurde besiegt.

Belohnungen

+16 XP
+12 Silber

Beute

[Räuberklinge Icon]
Räuberklinge
Selten

[ Zur Jagd ]

32. Reward presentation

Reward priority:

Victory
↓
XP
↓
Silver
↓
Item loot

Equipment drops should receive stronger visual emphasis than routine currency.

The UI should make a meaningful item drop feel valuable without using excessive mobile-game celebration effects.


33. Item cards

For dropped equipment, reuse or introduce a compact reusable item presentation.

Show:

icon
name
rarity
quantity if relevant

Do not implement the full comparison tooltip yet.

Do not show:

equip button
stat comparison
slot replacement

Those belong to Slice 0.5.


34. No-loot state

A victory without an item drop is valid.

The reward screen must still feel complete because XP and silver are guaranteed.

Example:

Keine besondere Beute gefunden.

Do not frame this as an error.


35. Character UI updates

After reward resolution, global UI elements showing character resources should refresh appropriately.

If the TopBar already displays XP or silver:

update from authoritative server data

Do not mutate those values optimistically without confirming the backend result.


36. Hunt return

After reviewing rewards:

Zur Jagd

returns to:

/hunt

The previous selected/consumed encounter must not become attackable again.

The player can start:

Neu suchen

for another hunt.


37. Database changes

Create or extend the migration for:

combat_reward
loot_table
loot_table_entry
character_item

and character currency fields where not already present.

Migration requirements:

  • preserve all existing data
  • use UUID primary keys
  • create proper foreign keys
  • enforce one reward per combat
  • create unique content keys
  • create useful indexes
  • do not drop unrelated tables
  • keep synchronize disabled

38. Seed changes

Extend the existing idempotent seed.

At minimum seed required loot configuration for:

Aschenratte
Straßenräuber

Seed relevant item definitions that do not already exist.

The seed must remain idempotent.

Running it repeatedly must not:

duplicate loot tables
duplicate loot entries
duplicate item definitions
reset player XP
reset silver
delete player items
delete combat rewards

39. Backend tests reward eligibility

Test:

ACTIVE combat
→ rewards rejected

Test:

LOST combat
→ rewards rejected

Test:

WON combat
→ rewards granted

40. Backend tests idempotency

Given one won combat:

resolve rewards
↓
resolve rewards again

Verify:

XP only granted once
silver only granted once
items only granted once
single CombatReward exists

This is one of the most important tests of the slice.


41. Backend tests Aschenratte

Using deterministic randomness, verify:

XP = 8
silver is within 47

Where implemented, verify expected item roll behavior.


42. Backend tests Straßenräuber

Verify:

XP = 16
silver is within 915

With deterministic random values, verify known item outcomes such as:

Räuberklinge drops

and:

Räuberklinge does not drop

No statistical/flaky tests.


43. Backend tests transaction

Simulate or test failure during reward persistence where practical.

Verify no state like:

XP granted
but reward record missing

or:

item granted twice

can result.


44. Backend tests CharacterItem

When an item is rolled:

CharacterItem persisted
correct character
correct ItemDefinition
correct quantity

45. Frontend tests victory rewards

Given:

status = WON
rewards.experience = 8
rewards.silver = 6

verify:

victory shown
8 XP shown
6 silver shown

46. Frontend tests item drop

Given reward:

Räuberklinge

verify:

item icon rendered
item name rendered
rarity rendered

Do not require an equip action yet.


47. Frontend tests no item

Given:

items = []

verify a valid no-special-loot state.


48. Frontend tests refresh

Opening an already won combat must reload its persisted rewards.

The frontend must not reroll or reconstruct them.


49. Server-authority checklist

The server owns:

reward eligibility
reward idempotency
XP amount
silver roll
loot tables
drop probabilities
random loot rolls
item granting
currency persistence
reward persistence

The client owns only:

displaying the reward
navigating away after the player reviews it

50. Visual requirements

The reward summary should remain part of the established Ashen Realms combat presentation.

Use:

dark metal / stone panels
bronze details
large readable reward values
high-quality item icons
restrained rarity emphasis

Do not introduce:

loot-box presentation
slot-machine effects
large flashing reward popups
bright mobile-game confetti
generic ecommerce cards

A valuable item should feel valuable primarily because of:

icon
rarity
name
presentation hierarchy

51. Definition of Done

Playable Slice 0.4 is complete when this full browser flow works:

Südtor
↓
Verbrannte Straße
↓
Jagd
↓
choose encounter
↓
combat
↓
win
↓
Combat = WON
↓
backend grants XP
↓
backend rolls and grants silver
↓
backend rolls loot
↓
any dropped item is persisted
↓
CombatReward persisted
↓
Angular shows reward summary
↓
refresh page
↓
same reward still shown
↓
no duplicate rewards granted
↓
return to hunt

52. Required verification

Before completion, run:

API unit tests
API integration tests
Angular tests
API build
Angular build
migration compilation
migration execution
seed execution

Perform browser walkthroughs for:

Aschenratte victory with no item
Aschenratte victory with forced deterministic item roll where supported
Straßenräuber victory
Straßenräuber with Räuberklinge drop
reward screen refresh
repeated reward request
return to hunt

53. Acceptance criteria

The slice is accepted when:

  • only WON combats receive victory rewards
  • one combat grants rewards at most once
  • XP is persisted
  • silver is persisted
  • silver ranges match the content definitions
  • loot rolls happen only on the backend
  • loot probabilities are data-driven
  • reward randomness is deterministic in tests
  • dropped items become persistent CharacterItems
  • reward data survives browser refresh
  • reward results never reroll on reload
  • Angular only renders server-provided reward state
  • item drops are visibly presented
  • a no-item victory is handled cleanly
  • CombatEngineService contains no reward logic
  • no equipment functionality is implemented prematurely
  • all tests pass
  • backend and frontend builds succeed

54. Handoff to Playable Slice 0.5

The exact handoff from Slice 0.4 is:

WON Combat
+
persisted CombatReward
+
persisted CharacterItem

For example:

Character owns:
Räuberklinge

but cannot yet equip it.

Playable Slice 0.5 will implement:

Inventory
→ inspect Räuberklinge
→ compare against Abgenutztes Kurzschwert
→ equip Räuberklinge
→ effective weapon damage increases
→ next combat becomes easier

55. Why Slice 0.5 matters

Slice 0.4 gives the player a reward.

Slice 0.5 makes that reward meaningful.

Together they complete the central progression loop:

hunt
→ fight
→ loot
→ equip
→ become stronger

That is the first real proof of the Ashen Realms item-driven progression philosophy.


56. Architectural boundary

Keep responsibilities separated:

CombatEngineService
    ↓
decides combat result

CombatService
    ↓
persists combat

Reward service
    ↓
decides and grants rewards

LootService
    ↓
rolls configured loot

CharacterItem
    ↓
stores player-owned loot

Slice 0.5
    ↓
uses that loot as equipment

Do not let the combat engine know about:

XP
silver
loot tables
inventory
equipment
item rarity

57. Summary

Playable Slice 0.4 proves that defeating an enemy produces real, persistent progression resources.

After this slice the player can:

travel
→ hunt
→ choose enemy
→ fight
→ win
→ receive XP
→ receive silver
→ find an item

The next slice completes the loop by allowing the player to actually use that item.