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

1641 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```text
World
→ Travel
→ Hunt
→ Encounter
→ Combat
→ Victory
```
to:
```text
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:
```text
Playable Slice 0.5 First Upgrade
Loot
→ Inventory
→ compare item
→ equip item
→ effective stats increase
```
---
# 2. Core player flow
The intended flow is:
```text
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:
```text
Sieg
Aschenratte besiegt
+8 XP
+6 Silber
Beute:
Aschenfell
```
or:
```text
Sieg
Straßenräuber besiegt
+16 XP
+12 Silber
Beute:
Räuberklinge
```
---
# 3. Scope
Implement:
```text
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:
```text
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:
```text
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:
```text
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:
```text
status = WON
```
A combat with:
```text
ACTIVE
```
or:
```text
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:
```text
win combat
receive loot
refresh browser
receive loot again
```
or:
```text
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:
```text
CombatReward
```
Required fields may include:
```ts
id: uuid;
combatId: uuid;
characterId: uuid;
experienceGranted: number;
silverGranted: number;
createdAt: timestamptz;
```
The relation:
```text
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:
```text
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:
```text
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:
```text
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:
```text
worn-short-sword
bandit-blade
bandit-hood
```
depending on the existing seed.
Required loot-relevant fields should already support or conceptually include:
```ts
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:
```ts
id: uuid;
characterId: uuid;
itemDefinitionId: uuid;
quantity: number;
createdAt: timestamptz;
updatedAt: timestamptz;
```
For equipment items, quantity may normally be:
```text
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:
```text
LootTable
```
Conceptual fields:
```ts
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:
```text
ash-rat-loot
road-bandit-loot
```
---
# 14. LootTableEntry
Create:
```text
LootTableEntry
```
Conceptual fields:
```ts
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
```text
47 Silver
8 XP
```
## Material
```text
60 % Aschenfell / Tierrest
```
## Equipment
```text
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
```text
915 Silver
16 XP
```
## Equipment
```text
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:
```text
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:
```text
roll Räuberklinge
roll Räuberhaube
```
This means one combat may produce:
```text
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:
```text
min = 4
max = 7
```
The backend rolls an inclusive integer:
```text
4
5
6
or
7
```
For Straßenräuber:
```text
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:
```text
encounter selection
silver roll
loot rolls
```
Tests must be able to inject fixed random values.
---
# 20. LootService
Create or extend:
```text
LootService
```
The LootService should be responsible for deterministic reward calculation from content definitions.
Conceptual API:
```ts
rollLoot(
monsterDefinitionId: string,
): Promise<LootRollResult>
```
The result may contain:
```ts
{
items: [
{
itemDefinitionId: string;
quantity: number;
}
]
}
```
Do not grant items inside the pure roll portion if that makes deterministic unit testing difficult.
Prefer separating:
```text
roll
```
from:
```text
grant/persist
```
where that matches existing architecture.
---
# 21. RewardService or orchestration boundary
Reward resolution should be orchestrated in one clear service boundary.
Possible name:
```text
CombatRewardService
```
or equivalent existing architecture.
Responsibilities:
```text
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:
```text
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:
```text
no partial reward should remain
```
---
# 23. When rewards are resolved
Preferred behavior:
When a combat action changes:
```text
ACTIVE
```
to:
```text
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:
```json
{
"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:
```text
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:
```ts
interface CombatRewardDto {
experience: number;
silver: number;
items: Array<{
characterItemId: string;
item: {
key: string;
name: string;
rarity: string;
iconPath: string;
};
quantity: number;
}>;
}
```
Do not expose:
```text
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:
```text
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:
```text
duplicate protection
salvaging
automatic conversion
pity system
```
in this slice.
---
# 29. No boss guarantee logic yet
The full balancing design later includes:
```text
guaranteed boss progression item
+
special boss roll
```
Do not implement that system for Slice 0.4.
The initial monsters are:
```text
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:
```text
Combat = WON
no rewards
```
Now:
```text
Combat = WON
reward resolution
Combat response contains persisted rewards
```
The combat engine itself must remain unaware of loot.
Keep this boundary:
```text
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:
```text
SIEG
Aschenratte wurde besiegt.
Belohnungen
+8 XP
+6 Silber
Keine besondere Beute gefunden.
[ Zur Jagd ]
```
If an item drops:
```text
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:
```text
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:
```text
icon
name
rarity
quantity if relevant
```
Do not implement the full comparison tooltip yet.
Do not show:
```text
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:
```text
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:
```text
update from authoritative server data
```
Do not mutate those values optimistically without confirming the backend result.
---
# 36. Hunt return
After reviewing rewards:
```text
Zur Jagd
```
returns to:
```text
/hunt
```
The previous selected/consumed encounter must not become attackable again.
The player can start:
```text
Neu suchen
```
for another hunt.
---
# 37. Database changes
Create or extend the migration for:
```text
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:
```text
Aschenratte
Straßenräuber
```
Seed relevant item definitions that do not already exist.
The seed must remain idempotent.
Running it repeatedly must not:
```text
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:
```text
ACTIVE combat
→ rewards rejected
```
Test:
```text
LOST combat
→ rewards rejected
```
Test:
```text
WON combat
→ rewards granted
```
---
# 40. Backend tests idempotency
Given one won combat:
```text
resolve rewards
resolve rewards again
```
Verify:
```text
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:
```text
XP = 8
silver is within 47
```
Where implemented, verify expected item roll behavior.
---
# 42. Backend tests Straßenräuber
Verify:
```text
XP = 16
silver is within 915
```
With deterministic random values, verify known item outcomes such as:
```text
Räuberklinge drops
```
and:
```text
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:
```text
XP granted
but reward record missing
```
or:
```text
item granted twice
```
can result.
---
# 44. Backend tests CharacterItem
When an item is rolled:
```text
CharacterItem persisted
correct character
correct ItemDefinition
correct quantity
```
---
# 45. Frontend tests victory rewards
Given:
```text
status = WON
rewards.experience = 8
rewards.silver = 6
```
verify:
```text
victory shown
8 XP shown
6 silver shown
```
---
# 46. Frontend tests item drop
Given reward:
```text
Räuberklinge
```
verify:
```text
item icon rendered
item name rendered
rarity rendered
```
Do not require an equip action yet.
---
# 47. Frontend tests no item
Given:
```text
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:
```text
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:
```text
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:
```text
dark metal / stone panels
bronze details
large readable reward values
high-quality item icons
restrained rarity emphasis
```
Do not introduce:
```text
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:
```text
icon
rarity
name
presentation hierarchy
```
---
# 51. Definition of Done
Playable Slice 0.4 is complete when this full browser flow works:
```text
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:
```text
API unit tests
API integration tests
Angular tests
API build
Angular build
migration compilation
migration execution
seed execution
```
Perform browser walkthroughs for:
```text
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:
```text
WON Combat
+
persisted CombatReward
+
persisted CharacterItem
```
For example:
```text
Character owns:
Räuberklinge
```
but cannot yet equip it.
Playable Slice 0.5 will implement:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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.