22 KiB
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
4–7 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
9–15 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:
9–15
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
synchronizedisabled
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 4–7
Where implemented, verify expected item roll behavior.
42. Backend tests – Straßenräuber
Verify:
XP = 16
silver is within 9–15
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.