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