1918 lines
25 KiB
Markdown
1918 lines
25 KiB
Markdown
# Ashen Realms – Playable Slice 0.3: First Combat
|
||
|
||
**Status:** Ready for implementation
|
||
**Prerequisite:** Playable Slice 0.2 – First Hunt
|
||
**Scope:** First persistent, server-authoritative combat
|
||
**Primary Enemies:** Aschenratte, Straßenräuber
|
||
**Next Slice:** Playable Slice 0.4 – First Loot
|
||
|
||
---
|
||
|
||
# 1. Goal
|
||
|
||
Playable Slice 0.3 implements the first real combat in Ashen Realms.
|
||
|
||
The existing flow:
|
||
|
||
```text
|
||
World
|
||
→ Travel
|
||
→ Hunt
|
||
→ Encounter selection
|
||
```
|
||
|
||
is extended to:
|
||
|
||
```text
|
||
World
|
||
→ Travel
|
||
→ Hunt
|
||
→ Encounter selection
|
||
→ Combat
|
||
→ Victory or defeat
|
||
```
|
||
|
||
The slice must prove that:
|
||
|
||
- combat can only start from a valid `HuntEncounter`
|
||
- combat state is persisted on the server
|
||
- the client sends decisions, never results
|
||
- combat resolution is deterministic
|
||
- player and monster HP are server-authoritative
|
||
- combat proceeds round by round
|
||
- combat events can drive frontend presentation
|
||
- victory and defeat are persisted
|
||
- refreshing the browser does not lose the fight
|
||
|
||
This slice deliberately ends when the combat reaches:
|
||
|
||
```text
|
||
WON
|
||
```
|
||
|
||
or:
|
||
|
||
```text
|
||
LOST
|
||
```
|
||
|
||
No rewards are granted yet.
|
||
|
||
---
|
||
|
||
# 2. Core player flow
|
||
|
||
```text
|
||
Verbrannte Straße
|
||
↓
|
||
Jagd beginnen
|
||
↓
|
||
Encounter auswählen
|
||
↓
|
||
Angreifen
|
||
↓
|
||
Server validates HuntEncounter
|
||
↓
|
||
Combat created
|
||
↓
|
||
/combat/:combatId
|
||
↓
|
||
Player and monster are displayed
|
||
↓
|
||
Player chooses Angriff
|
||
↓
|
||
Server resolves round
|
||
↓
|
||
CombatEvents returned
|
||
↓
|
||
Frontend updates HP and combat log
|
||
↓
|
||
next round
|
||
↓
|
||
monster reaches 0 HP
|
||
↓
|
||
Combat = WON
|
||
```
|
||
|
||
Defeat must also work:
|
||
|
||
```text
|
||
player reaches 0 HP
|
||
↓
|
||
Combat = LOST
|
||
```
|
||
|
||
---
|
||
|
||
# 3. Scope
|
||
|
||
Implement:
|
||
|
||
```text
|
||
Combat entity
|
||
CombatEvent entity
|
||
CombatEngineService
|
||
CombatService
|
||
CombatController
|
||
|
||
combat creation from HuntEncounter
|
||
|
||
ATTACK player action
|
||
|
||
basic monster attack
|
||
|
||
server-side damage calculation
|
||
|
||
round progression
|
||
|
||
player HP
|
||
|
||
monster HP
|
||
|
||
combat status
|
||
|
||
combat event persistence
|
||
|
||
combat page
|
||
|
||
player presentation
|
||
|
||
monster presentation
|
||
|
||
HP bars
|
||
|
||
Angriff action
|
||
|
||
round display
|
||
|
||
combat log
|
||
|
||
victory state
|
||
|
||
defeat state
|
||
```
|
||
|
||
---
|
||
|
||
# 4. Explicit non-goals
|
||
|
||
Do not implement:
|
||
|
||
```text
|
||
loot
|
||
XP granting
|
||
silver granting
|
||
inventory
|
||
equipment management
|
||
item drops
|
||
quests
|
||
bosses
|
||
elites
|
||
sets
|
||
area currencies
|
||
random combat rewards
|
||
combat animations requiring complex timing
|
||
WebSockets
|
||
PvP
|
||
multiplayer combat
|
||
```
|
||
|
||
Also do not implement the complete final action system yet.
|
||
|
||
The first playable combat uses only:
|
||
|
||
```text
|
||
ATTACK
|
||
```
|
||
|
||
for player input.
|
||
|
||
Future slices may add:
|
||
|
||
```text
|
||
HEAVY_STRIKE
|
||
SHIELD_BASH
|
||
DEFEND
|
||
POTION
|
||
FLEE
|
||
```
|
||
|
||
The combat architecture must allow these later without implementing them prematurely.
|
||
|
||
---
|
||
|
||
# 5. Combat design principle
|
||
|
||
Combat is turn-based.
|
||
|
||
The player makes exactly one primary decision per round.
|
||
|
||
For Slice 0.3:
|
||
|
||
```text
|
||
Player chooses ATTACK
|
||
↓
|
||
player attack resolves
|
||
↓
|
||
if monster survives:
|
||
monster attack resolves
|
||
↓
|
||
round ends
|
||
```
|
||
|
||
The next round begins only after the server has resolved the previous round.
|
||
|
||
Angular must never independently resolve a combat round.
|
||
|
||
---
|
||
|
||
# 6. Combat start boundary
|
||
|
||
A combat may only be created from:
|
||
|
||
```text
|
||
HuntEncounter.id
|
||
```
|
||
|
||
Never from:
|
||
|
||
```text
|
||
MonsterDefinition.id
|
||
```
|
||
|
||
directly supplied by the frontend.
|
||
|
||
Required endpoint:
|
||
|
||
```http
|
||
POST /api/hunt-encounters/:encounterId/attack
|
||
```
|
||
|
||
The backend must validate:
|
||
|
||
```text
|
||
HuntEncounter exists
|
||
↓
|
||
encounter belongs to current character's Hunt
|
||
↓
|
||
Hunt belongs to current character
|
||
↓
|
||
Hunt is valid/current
|
||
↓
|
||
encounter has not already been consumed
|
||
↓
|
||
character is not travelling
|
||
↓
|
||
character does not already have another ACTIVE combat
|
||
```
|
||
|
||
Only then may combat be created.
|
||
|
||
---
|
||
|
||
# 7. Encounter consumption
|
||
|
||
A HuntEncounter must not be usable repeatedly to create unlimited combats.
|
||
|
||
Introduce an appropriate state or relation.
|
||
|
||
Possible states:
|
||
|
||
```text
|
||
AVAILABLE
|
||
SELECTED
|
||
CONSUMED
|
||
```
|
||
|
||
For this slice it is sufficient if the selected encounter becomes:
|
||
|
||
```text
|
||
CONSUMED
|
||
```
|
||
|
||
when combat is successfully created.
|
||
|
||
The exact persistence model may follow existing project conventions.
|
||
|
||
The important invariant is:
|
||
|
||
> One HuntEncounter may create at most one Combat.
|
||
|
||
Prefer protecting this through both service validation and a database uniqueness constraint where practical.
|
||
|
||
---
|
||
|
||
# 8. Combat entity
|
||
|
||
Create:
|
||
|
||
```text
|
||
apps/api/src/combat/entities/combat.entity.ts
|
||
```
|
||
|
||
Required conceptual fields:
|
||
|
||
```ts
|
||
id: uuid;
|
||
|
||
characterId: uuid;
|
||
huntEncounterId: uuid;
|
||
monsterDefinitionId: uuid;
|
||
|
||
status: CombatStatus;
|
||
|
||
round: number;
|
||
|
||
playerMaxHp: number;
|
||
playerCurrentHp: number;
|
||
|
||
monsterMaxHp: number;
|
||
monsterCurrentHp: number;
|
||
|
||
playerState: jsonb;
|
||
monsterState: jsonb;
|
||
|
||
createdAt: timestamptz;
|
||
updatedAt: timestamptz;
|
||
completedAt?: timestamptz;
|
||
```
|
||
|
||
Supported statuses:
|
||
|
||
```text
|
||
ACTIVE
|
||
WON
|
||
LOST
|
||
```
|
||
|
||
Do not add reward-related states yet.
|
||
|
||
---
|
||
|
||
# 9. Combat snapshot principle
|
||
|
||
Combat must snapshot the relevant stats at combat creation.
|
||
|
||
A running combat must not unexpectedly change because content definitions or character state are edited elsewhere.
|
||
|
||
The combat snapshot should contain enough information to reproduce its deterministic calculations.
|
||
|
||
Conceptually:
|
||
|
||
```ts
|
||
playerState = {
|
||
attack: number,
|
||
weaponDamage: number,
|
||
armor: number
|
||
}
|
||
|
||
monsterState = {
|
||
attack: number,
|
||
armor: number
|
||
}
|
||
```
|
||
|
||
The exact JSON structure may be strongly typed in TypeScript.
|
||
|
||
Avoid unstructured arbitrary JSON access throughout the engine.
|
||
|
||
---
|
||
|
||
# 10. Player stats for the first combat
|
||
|
||
The final game uses effective character values derived from:
|
||
|
||
```text
|
||
base character stats
|
||
+
|
||
equipment
|
||
```
|
||
|
||
For the first combat slice, reuse `CharacterStatsService` if it already exists.
|
||
|
||
If equipment has not yet been implemented, do not implement the entire inventory/equipment system solely for Slice 0.3.
|
||
|
||
The demo character starts conceptually with:
|
||
|
||
```text
|
||
100 HP
|
||
6 base attack
|
||
8 weapon damage
|
||
6 armor
|
||
```
|
||
|
||
These correspond to the established starting reference character.
|
||
|
||
Any temporary mechanism needed to supply the starting weapon/armor values must:
|
||
|
||
- remain server-side
|
||
- be isolated behind the character/combat stats boundary
|
||
- not leak into Angular
|
||
- be easy to replace when Slice 0.5 introduces real equipment
|
||
- not change the public combat API
|
||
|
||
Do not create fake frontend equipment state.
|
||
|
||
---
|
||
|
||
# 11. Damage formula
|
||
|
||
Use the established Ashen Realms damage model.
|
||
|
||
Raw damage:
|
||
|
||
```text
|
||
Raw Damage =
|
||
Weapon Damage + Attack
|
||
```
|
||
|
||
Armor mitigation:
|
||
|
||
```text
|
||
Damage =
|
||
Raw Damage × 60 / (60 + Armor)
|
||
```
|
||
|
||
Round to the nearest integer.
|
||
|
||
Minimum damage:
|
||
|
||
```text
|
||
1
|
||
```
|
||
|
||
Conceptual implementation:
|
||
|
||
```ts
|
||
const rawDamage =
|
||
attack + weaponDamage;
|
||
|
||
const mitigatedDamage =
|
||
rawDamage * 60 / (60 + targetArmor);
|
||
|
||
return Math.max(
|
||
1,
|
||
Math.round(mitigatedDamage),
|
||
);
|
||
```
|
||
|
||
For monsters without explicit weapon damage, their `attack` may represent their complete offensive base value for this first slice.
|
||
|
||
Keep this distinction explicit in engine types.
|
||
|
||
---
|
||
|
||
# 12. No combat randomness
|
||
|
||
Slice 0.3 combat contains no random damage range.
|
||
|
||
Do not implement:
|
||
|
||
```text
|
||
critical hits
|
||
dodge
|
||
block chance
|
||
accuracy
|
||
hit chance
|
||
random damage variance
|
||
elemental damage
|
||
resistances
|
||
```
|
||
|
||
Given the same combat state and action:
|
||
|
||
> the same result must be produced.
|
||
|
||
This is intentional.
|
||
|
||
---
|
||
|
||
# 13. CombatEngineService
|
||
|
||
Create a framework-light engine:
|
||
|
||
```text
|
||
apps/api/src/combat/combat-engine.service.ts
|
||
```
|
||
|
||
The engine should not directly access PostgreSQL.
|
||
|
||
Required conceptual API:
|
||
|
||
```ts
|
||
resolveAction(
|
||
state: CombatEngineState,
|
||
action: CombatActionInput,
|
||
): CombatEngineResult
|
||
```
|
||
|
||
The engine receives:
|
||
|
||
```text
|
||
current round
|
||
player stats/state
|
||
monster stats/state
|
||
requested player action
|
||
```
|
||
|
||
and returns:
|
||
|
||
```text
|
||
new combat state
|
||
events
|
||
status/result
|
||
```
|
||
|
||
The engine must be unit-testable without Nest repositories.
|
||
|
||
---
|
||
|
||
# 14. Combat action enum
|
||
|
||
Introduce:
|
||
|
||
```text
|
||
ATTACK
|
||
```
|
||
|
||
as a real enum/API value.
|
||
|
||
Design the enum so future values can be added:
|
||
|
||
```text
|
||
HEAVY_STRIKE
|
||
SHIELD_BASH
|
||
DEFEND
|
||
POTION
|
||
FLEE
|
||
```
|
||
|
||
But do not implement their behavior in Slice 0.3.
|
||
|
||
If an unsupported action is submitted, return a domain validation error.
|
||
|
||
---
|
||
|
||
# 15. Round resolution
|
||
|
||
For:
|
||
|
||
```text
|
||
ATTACK
|
||
```
|
||
|
||
resolve:
|
||
|
||
## Step 1 – Player attack
|
||
|
||
Calculate damage against monster armor.
|
||
|
||
Reduce:
|
||
|
||
```text
|
||
monsterCurrentHp
|
||
```
|
||
|
||
but never below:
|
||
|
||
```text
|
||
0
|
||
```
|
||
|
||
Create a combat event.
|
||
|
||
---
|
||
|
||
## Step 2 – Victory check
|
||
|
||
If:
|
||
|
||
```text
|
||
monsterCurrentHp <= 0
|
||
```
|
||
|
||
then:
|
||
|
||
```text
|
||
status = WON
|
||
```
|
||
|
||
The monster must not receive another attack.
|
||
|
||
The round ends.
|
||
|
||
---
|
||
|
||
## Step 3 – Monster attack
|
||
|
||
If the monster survived:
|
||
|
||
Calculate monster damage against player armor.
|
||
|
||
Reduce:
|
||
|
||
```text
|
||
playerCurrentHp
|
||
```
|
||
|
||
but never below:
|
||
|
||
```text
|
||
0
|
||
```
|
||
|
||
Create a combat event.
|
||
|
||
---
|
||
|
||
## Step 4 – Defeat check
|
||
|
||
If:
|
||
|
||
```text
|
||
playerCurrentHp <= 0
|
||
```
|
||
|
||
then:
|
||
|
||
```text
|
||
status = LOST
|
||
```
|
||
|
||
---
|
||
|
||
## Step 5 – Round progression
|
||
|
||
If combat remains active:
|
||
|
||
```text
|
||
round += 1
|
||
```
|
||
|
||
Use one consistent round-number convention throughout API, persistence and UI.
|
||
|
||
Recommended:
|
||
|
||
```text
|
||
Combat starts at round 1.
|
||
After resolving round 1 successfully:
|
||
round becomes 2.
|
||
```
|
||
|
||
---
|
||
|
||
# 16. CombatEvent entity
|
||
|
||
Create:
|
||
|
||
```text
|
||
apps/api/src/combat/entities/combat-event.entity.ts
|
||
```
|
||
|
||
Fields should support ordered event history.
|
||
|
||
Conceptually:
|
||
|
||
```ts
|
||
id: uuid;
|
||
|
||
combatId: uuid;
|
||
|
||
round: number;
|
||
sequence: number;
|
||
|
||
type: CombatEventType;
|
||
|
||
source: PLAYER | MONSTER;
|
||
target: PLAYER | MONSTER;
|
||
|
||
amount?: number;
|
||
|
||
createdAt: timestamptz;
|
||
```
|
||
|
||
Initial event types should include:
|
||
|
||
```text
|
||
DAMAGE
|
||
COMBAT_WON
|
||
COMBAT_LOST
|
||
```
|
||
|
||
Avoid storing presentation sentences as the authoritative event format.
|
||
|
||
Store structured events.
|
||
|
||
Angular may transform them into German display text.
|
||
|
||
---
|
||
|
||
# 17. Example events
|
||
|
||
Player attacks Aschenratte:
|
||
|
||
```json
|
||
{
|
||
"type": "DAMAGE",
|
||
"source": "PLAYER",
|
||
"target": "MONSTER",
|
||
"amount": 14
|
||
}
|
||
```
|
||
|
||
Monster attacks player:
|
||
|
||
```json
|
||
{
|
||
"type": "DAMAGE",
|
||
"source": "MONSTER",
|
||
"target": "PLAYER",
|
||
"amount": 5
|
||
}
|
||
```
|
||
|
||
Combat victory:
|
||
|
||
```json
|
||
{
|
||
"type": "COMBAT_WON",
|
||
"source": "PLAYER",
|
||
"target": "MONSTER"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
# 18. Combat creation transaction
|
||
|
||
Creating combat should be atomic.
|
||
|
||
Conceptual transaction:
|
||
|
||
```text
|
||
lock/validate HuntEncounter
|
||
↓
|
||
verify no active combat
|
||
↓
|
||
load monster
|
||
↓
|
||
calculate/snapshot player stats
|
||
↓
|
||
create Combat
|
||
↓
|
||
consume HuntEncounter
|
||
↓
|
||
commit
|
||
```
|
||
|
||
Concurrent requests for the same encounter must not create two combats.
|
||
|
||
---
|
||
|
||
# 19. CombatService
|
||
|
||
`CombatService` owns persistence and orchestration.
|
||
|
||
Responsibilities:
|
||
|
||
```text
|
||
create combat from encounter
|
||
|
||
load combat
|
||
|
||
validate combat ownership/state
|
||
|
||
call CombatEngineService
|
||
|
||
persist updated HP/state
|
||
|
||
persist ordered CombatEvents
|
||
|
||
persist WON/LOST status
|
||
|
||
return combat DTO
|
||
```
|
||
|
||
Do not put damage formulas into the controller.
|
||
|
||
Do not put TypeORM repository access into the pure combat engine.
|
||
|
||
---
|
||
|
||
# 20. Combat API
|
||
|
||
Required endpoints:
|
||
|
||
```http
|
||
POST /api/hunt-encounters/:encounterId/attack
|
||
|
||
GET /api/combats/:combatId
|
||
|
||
POST /api/combats/:combatId/actions
|
||
```
|
||
|
||
---
|
||
|
||
# 21. Start combat request
|
||
|
||
Example:
|
||
|
||
```http
|
||
POST /api/hunt-encounters/abc123/attack
|
||
```
|
||
|
||
No body containing combat values is required.
|
||
|
||
The server derives:
|
||
|
||
```text
|
||
character
|
||
monster
|
||
player stats
|
||
monster stats
|
||
starting HP
|
||
```
|
||
|
||
Response should return the newly created combat.
|
||
|
||
---
|
||
|
||
# 22. Combat action request
|
||
|
||
Request:
|
||
|
||
```json
|
||
{
|
||
"action": "ATTACK"
|
||
}
|
||
```
|
||
|
||
The frontend must never send:
|
||
|
||
```text
|
||
damage
|
||
playerHp
|
||
monsterHp
|
||
armor
|
||
attack
|
||
weaponDamage
|
||
round
|
||
combat status
|
||
```
|
||
|
||
These values are entirely server-owned.
|
||
|
||
---
|
||
|
||
# 23. Combat response DTO
|
||
|
||
Conceptually:
|
||
|
||
```json
|
||
{
|
||
"id": "combat-uuid",
|
||
"status": "ACTIVE",
|
||
"round": 2,
|
||
|
||
"player": {
|
||
"name": "Aric Duskwalker",
|
||
"maxHp": 100,
|
||
"currentHp": 95
|
||
},
|
||
|
||
"monster": {
|
||
"key": "ash-rat",
|
||
"name": "Aschenratte",
|
||
"level": 1,
|
||
"maxHp": 45,
|
||
"currentHp": 31,
|
||
"artworkPath": "/assets/monsters/ash-rat.webp"
|
||
},
|
||
|
||
"events": [
|
||
{
|
||
"round": 1,
|
||
"sequence": 1,
|
||
"type": "DAMAGE",
|
||
"source": "PLAYER",
|
||
"target": "MONSTER",
|
||
"amount": 14
|
||
},
|
||
{
|
||
"round": 1,
|
||
"sequence": 2,
|
||
"type": "DAMAGE",
|
||
"source": "MONSTER",
|
||
"target": "PLAYER",
|
||
"amount": 5
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Do not expose unnecessary internal engine state.
|
||
|
||
---
|
||
|
||
# 24. GET combat
|
||
|
||
`GET /api/combats/:combatId` must allow the browser to restore the fight after:
|
||
|
||
```text
|
||
refresh
|
||
route reload
|
||
browser reconnect
|
||
```
|
||
|
||
The frontend must not depend on transient local state for current HP or round.
|
||
|
||
The server response remains authoritative.
|
||
|
||
---
|
||
|
||
# 25. Finished combat behavior
|
||
|
||
If combat status is:
|
||
|
||
```text
|
||
WON
|
||
```
|
||
|
||
or:
|
||
|
||
```text
|
||
LOST
|
||
```
|
||
|
||
then further calls to:
|
||
|
||
```http
|
||
POST /api/combats/:combatId/actions
|
||
```
|
||
|
||
must be rejected.
|
||
|
||
Example error:
|
||
|
||
```json
|
||
{
|
||
"statusCode": 409,
|
||
"code": "COMBAT_ALREADY_FINISHED",
|
||
"message": "This combat has already finished."
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
# 26. Active combat restriction
|
||
|
||
A character may have at most one:
|
||
|
||
```text
|
||
ACTIVE
|
||
```
|
||
|
||
combat.
|
||
|
||
Starting another encounter while a combat is active must fail.
|
||
|
||
Example:
|
||
|
||
```text
|
||
COMBAT_ALREADY_ACTIVE
|
||
```
|
||
|
||
Where practical, protect this invariant using a partial unique database index in addition to service validation.
|
||
|
||
---
|
||
|
||
# 27. Invalid encounter errors
|
||
|
||
Use stable domain errors.
|
||
|
||
Examples:
|
||
|
||
```text
|
||
HUNT_ENCOUNTER_NOT_FOUND
|
||
HUNT_ENCOUNTER_ALREADY_CONSUMED
|
||
INVALID_HUNT_ENCOUNTER
|
||
CHARACTER_TRAVELLING
|
||
COMBAT_ALREADY_ACTIVE
|
||
```
|
||
|
||
Do not expose raw TypeORM/PostgreSQL errors to the client.
|
||
|
||
---
|
||
|
||
# 28. Database migration
|
||
|
||
Create a reviewed TypeORM migration for:
|
||
|
||
```text
|
||
combat
|
||
combat_event
|
||
```
|
||
|
||
and any required HuntEncounter state/constraint changes.
|
||
|
||
The migration must:
|
||
|
||
- preserve existing world/travel/hunting data
|
||
- use UUID primary keys
|
||
- create foreign keys
|
||
- create useful indexes
|
||
- prevent obvious duplicate active-combat states where practical
|
||
- not recreate unrelated tables
|
||
- keep `synchronize` disabled
|
||
|
||
---
|
||
|
||
# 29. Angular route
|
||
|
||
Implement:
|
||
|
||
```text
|
||
/combat/:combatId
|
||
```
|
||
|
||
The Hunt screen's:
|
||
|
||
```text
|
||
Angreifen
|
||
```
|
||
|
||
action changes from the Slice 0.2 placeholder to:
|
||
|
||
```text
|
||
POST combat from encounter
|
||
↓
|
||
receive combat ID
|
||
↓
|
||
navigate to /combat/:combatId
|
||
```
|
||
|
||
Remove the temporary combat placeholder from Slice 0.2.
|
||
|
||
---
|
||
|
||
# 30. Combat screen composition
|
||
|
||
The combat screen follows the established Ashen Realms layout:
|
||
|
||
```text
|
||
player left
|
||
monster right
|
||
location background
|
||
combat actions below
|
||
combat information clearly visible
|
||
combat log in contextual area/right side
|
||
```
|
||
|
||
The fight must visually feel like a confrontation.
|
||
|
||
Do not render combat as a statistics table.
|
||
|
||
---
|
||
|
||
# 31. Player presentation
|
||
|
||
Display:
|
||
|
||
```text
|
||
character artwork
|
||
character name
|
||
current HP
|
||
maximum HP
|
||
HP bar
|
||
```
|
||
|
||
Use existing player artwork.
|
||
|
||
Do not introduce a new art style.
|
||
|
||
---
|
||
|
||
# 32. Monster presentation
|
||
|
||
Display:
|
||
|
||
```text
|
||
monster artwork
|
||
monster name
|
||
monster level
|
||
current HP
|
||
maximum HP
|
||
HP bar
|
||
```
|
||
|
||
Use the artwork path supplied by the API/content definition.
|
||
|
||
The monster should receive similar visual weight to the player.
|
||
|
||
---
|
||
|
||
# 33. Background
|
||
|
||
Use the current combat location:
|
||
|
||
```text
|
||
Verbrannte Straße
|
||
```
|
||
|
||
as the scene context.
|
||
|
||
Reuse the existing location artwork where appropriate.
|
||
|
||
The background must support the confrontation rather than compete with HP/action readability.
|
||
|
||
---
|
||
|
||
# 34. Combat action bar
|
||
|
||
Slice 0.3 shows one functional action:
|
||
|
||
```text
|
||
Angriff
|
||
```
|
||
|
||
The action should be implemented using the visual language intended for future combat buttons.
|
||
|
||
Recommended display:
|
||
|
||
```text
|
||
icon
|
||
Angriff
|
||
optional hotkey
|
||
```
|
||
|
||
Do not show fake functional buttons for unimplemented mechanics.
|
||
|
||
It is acceptable to reserve visual space for future actions, but disabled placeholders should only be used if they improve layout validation.
|
||
|
||
---
|
||
|
||
# 35. Input locking
|
||
|
||
When an action request is in progress:
|
||
|
||
```text
|
||
disable combat actions
|
||
```
|
||
|
||
The player must not accidentally submit the same round multiple times.
|
||
|
||
The backend must still protect against concurrency.
|
||
|
||
Frontend locking is only UX protection.
|
||
|
||
Server validation remains authoritative.
|
||
|
||
---
|
||
|
||
# 36. Combat log
|
||
|
||
Display structured events as readable German log entries.
|
||
|
||
Example:
|
||
|
||
```text
|
||
Runde 1
|
||
|
||
Aric trifft Aschenratte für 14 Schaden.
|
||
Aschenratte trifft Aric für 5 Schaden.
|
||
```
|
||
|
||
Do not persist these sentences as domain truth.
|
||
|
||
Build them from structured `CombatEventDto` objects.
|
||
|
||
---
|
||
|
||
# 37. Round display
|
||
|
||
Clearly show:
|
||
|
||
```text
|
||
Runde 1
|
||
Runde 2
|
||
...
|
||
```
|
||
|
||
Round state must come from the API.
|
||
|
||
Angular must never independently increment the authoritative round value.
|
||
|
||
---
|
||
|
||
# 38. Victory state
|
||
|
||
When:
|
||
|
||
```text
|
||
status = WON
|
||
```
|
||
|
||
show a clear victory state.
|
||
|
||
Example:
|
||
|
||
```text
|
||
Sieg
|
||
|
||
Die Aschenratte wurde besiegt.
|
||
```
|
||
|
||
At this point Slice 0.3 ends.
|
||
|
||
Do not grant or invent rewards.
|
||
|
||
A small deliberate message may indicate:
|
||
|
||
```text
|
||
Belohnungen werden im nächsten Schritt verarbeitet.
|
||
```
|
||
|
||
Primary action may be:
|
||
|
||
```text
|
||
Zur Jagd
|
||
```
|
||
|
||
Do not automatically create another hunt.
|
||
|
||
---
|
||
|
||
# 39. Defeat state
|
||
|
||
When:
|
||
|
||
```text
|
||
status = LOST
|
||
```
|
||
|
||
show a clear defeat state.
|
||
|
||
Example:
|
||
|
||
```text
|
||
Niederlage
|
||
|
||
Aric wurde im Kampf besiegt.
|
||
```
|
||
|
||
For Slice 0.3, do not build the complete long-term defeat/reset system unless it already exists.
|
||
|
||
The combat must remain persisted as:
|
||
|
||
```text
|
||
LOST
|
||
```
|
||
|
||
A simple return action to the world/hunt context is sufficient for this slice.
|
||
|
||
Any final safe-location reset should be defined in a later dedicated slice unless already implemented.
|
||
|
||
---
|
||
|
||
# 40. Refresh/recovery behavior
|
||
|
||
Refreshing:
|
||
|
||
```text
|
||
/combat/:combatId
|
||
```
|
||
|
||
must reload combat from the server.
|
||
|
||
The UI must restore:
|
||
|
||
```text
|
||
combat status
|
||
round
|
||
player HP
|
||
monster HP
|
||
monster
|
||
combat history
|
||
```
|
||
|
||
No combat-critical state may exist only in Angular memory.
|
||
|
||
---
|
||
|
||
# 41. Frontend state
|
||
|
||
Recommended state:
|
||
|
||
```ts
|
||
combat
|
||
loading
|
||
actionPending
|
||
error
|
||
```
|
||
|
||
Derived presentation state may include:
|
||
|
||
```text
|
||
player hp percentage
|
||
monster hp percentage
|
||
formatted combat events
|
||
```
|
||
|
||
Do not duplicate authoritative gameplay values in mutable frontend state.
|
||
|
||
---
|
||
|
||
# 42. Combat API service
|
||
|
||
Add focused methods equivalent to:
|
||
|
||
```ts
|
||
startCombat(
|
||
encounterId: string,
|
||
): Observable<CombatDto>
|
||
|
||
getCombat(
|
||
combatId: string,
|
||
): Observable<CombatDto>
|
||
|
||
performAction(
|
||
combatId: string,
|
||
action: CombatAction,
|
||
): Observable<CombatDto>
|
||
```
|
||
|
||
Only send the action enum for combat actions.
|
||
|
||
---
|
||
|
||
# 43. CombatEngine unit tests
|
||
|
||
At minimum cover:
|
||
|
||
## Damage formula
|
||
|
||
Example established case:
|
||
|
||
```text
|
||
attack = 12
|
||
weaponDamage = 15
|
||
target armor = 20
|
||
```
|
||
|
||
Expected:
|
||
|
||
```text
|
||
20 damage
|
||
```
|
||
|
||
---
|
||
|
||
## Minimum damage
|
||
|
||
Extremely high armor must still result in:
|
||
|
||
```text
|
||
1 damage minimum
|
||
```
|
||
|
||
---
|
||
|
||
## Player attack
|
||
|
||
Verify:
|
||
|
||
```text
|
||
monster HP decreases correctly
|
||
DAMAGE event created
|
||
```
|
||
|
||
---
|
||
|
||
## Monster retaliation
|
||
|
||
If the monster survives:
|
||
|
||
```text
|
||
player HP decreases
|
||
monster DAMAGE event created
|
||
```
|
||
|
||
---
|
||
|
||
## No retaliation after death
|
||
|
||
If player attack reduces monster to zero:
|
||
|
||
```text
|
||
monster does not attack
|
||
combat = WON
|
||
```
|
||
|
||
---
|
||
|
||
## Defeat
|
||
|
||
If monster attack reduces player to zero:
|
||
|
||
```text
|
||
combat = LOST
|
||
```
|
||
|
||
---
|
||
|
||
## Determinism
|
||
|
||
The same state plus:
|
||
|
||
```text
|
||
ATTACK
|
||
```
|
||
|
||
must produce the same result.
|
||
|
||
---
|
||
|
||
# 44. CombatService tests
|
||
|
||
At minimum cover:
|
||
|
||
## Valid encounter starts combat
|
||
|
||
```text
|
||
available HuntEncounter
|
||
→ ACTIVE Combat
|
||
```
|
||
|
||
---
|
||
|
||
## Arbitrary monster cannot start combat
|
||
|
||
There must be no public API accepting an arbitrary monster definition as the combat source.
|
||
|
||
---
|
||
|
||
## Encounter can only be consumed once
|
||
|
||
Two attempts using the same encounter must not produce two combats.
|
||
|
||
---
|
||
|
||
## Active combat restriction
|
||
|
||
Character with an ACTIVE combat cannot create another.
|
||
|
||
---
|
||
|
||
## Persistence
|
||
|
||
After an action:
|
||
|
||
```text
|
||
HP changes persisted
|
||
round persisted
|
||
CombatEvents persisted
|
||
```
|
||
|
||
---
|
||
|
||
## Finished combat
|
||
|
||
Further actions after:
|
||
|
||
```text
|
||
WON
|
||
```
|
||
|
||
or:
|
||
|
||
```text
|
||
LOST
|
||
```
|
||
|
||
must fail.
|
||
|
||
---
|
||
|
||
## Transaction/concurrency
|
||
|
||
Where feasible, verify concurrent or repeated requests cannot resolve the same combat round twice.
|
||
|
||
---
|
||
|
||
# 45. Frontend tests
|
||
|
||
At minimum cover:
|
||
|
||
## Combat creation
|
||
|
||
Clicking:
|
||
|
||
```text
|
||
Angreifen
|
||
```
|
||
|
||
on the Hunt page:
|
||
|
||
```text
|
||
uses HuntEncounter.id
|
||
calls combat creation
|
||
navigates to /combat/:combatId
|
||
```
|
||
|
||
---
|
||
|
||
## Combat load
|
||
|
||
Opening:
|
||
|
||
```text
|
||
/combat/:combatId
|
||
```
|
||
|
||
loads the authoritative combat state.
|
||
|
||
---
|
||
|
||
## Combat presentation
|
||
|
||
Verify visibility of:
|
||
|
||
```text
|
||
player
|
||
monster
|
||
both HP bars
|
||
round
|
||
Angriff
|
||
combat log
|
||
```
|
||
|
||
---
|
||
|
||
## Combat action
|
||
|
||
Click:
|
||
|
||
```text
|
||
Angriff
|
||
```
|
||
|
||
and verify the frontend sends only:
|
||
|
||
```json
|
||
{
|
||
"action": "ATTACK"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Action lock
|
||
|
||
While the action request is unresolved:
|
||
|
||
```text
|
||
Angriff disabled
|
||
```
|
||
|
||
---
|
||
|
||
## Event rendering
|
||
|
||
Structured DAMAGE events are rendered as readable combat-log entries.
|
||
|
||
---
|
||
|
||
## Victory
|
||
|
||
When API returns:
|
||
|
||
```text
|
||
WON
|
||
```
|
||
|
||
the UI shows victory and disables combat actions.
|
||
|
||
---
|
||
|
||
## Defeat
|
||
|
||
When API returns:
|
||
|
||
```text
|
||
LOST
|
||
```
|
||
|
||
the UI shows defeat and disables combat actions.
|
||
|
||
---
|
||
|
||
# 46. Visual requirements
|
||
|
||
Follow the existing Ashen Realms visual design.
|
||
|
||
Combat should emphasize:
|
||
|
||
```text
|
||
large character artwork
|
||
large monster artwork
|
||
dark fantasy location background
|
||
clear HP bars
|
||
clear action area
|
||
strong confrontation
|
||
dark metal/stone panels
|
||
bronze detail
|
||
restrained functional colors
|
||
```
|
||
|
||
Do not use:
|
||
|
||
```text
|
||
generic cards
|
||
SaaS dashboard layout
|
||
white panels
|
||
Material defaults
|
||
Bootstrap defaults
|
||
glassmorphism
|
||
neon-heavy effects
|
||
```
|
||
|
||
---
|
||
|
||
# 47. Animation scope
|
||
|
||
Simple presentation effects are allowed:
|
||
|
||
```text
|
||
small hit reaction
|
||
short damage number
|
||
subtle impact animation
|
||
HP-bar transition
|
||
```
|
||
|
||
But animations must be driven from server-returned events.
|
||
|
||
Do not delay or alter authoritative combat resolution for presentation.
|
||
|
||
Do not introduce a complex combat animation queue unless the existing frontend architecture already supports one cleanly.
|
||
|
||
Respect:
|
||
|
||
```css
|
||
prefers-reduced-motion
|
||
```
|
||
|
||
---
|
||
|
||
# 48. Server-authority checklist
|
||
|
||
Server decides:
|
||
|
||
```text
|
||
whether combat can start
|
||
which monster belongs to encounter
|
||
combat starting stats
|
||
player HP
|
||
monster HP
|
||
damage
|
||
round progression
|
||
monster retaliation
|
||
victory
|
||
defeat
|
||
combat status
|
||
combat events
|
||
```
|
||
|
||
Client decides only:
|
||
|
||
```text
|
||
which valid HuntEncounter to attack
|
||
when to submit ATTACK during an active combat
|
||
```
|
||
|
||
---
|
||
|
||
# 49. Security/domain constraints
|
||
|
||
The client must never be able to submit:
|
||
|
||
```text
|
||
monsterId
|
||
damage
|
||
target HP
|
||
player HP
|
||
armor
|
||
attack
|
||
weapon damage
|
||
victory
|
||
defeat
|
||
reward
|
||
round number
|
||
```
|
||
|
||
as authoritative combat inputs.
|
||
|
||
Do not trust disabled frontend buttons as domain validation.
|
||
|
||
Every action must be validated server-side.
|
||
|
||
---
|
||
|
||
# 50. Definition of Done
|
||
|
||
Playable Slice 0.3 is complete when this complete browser flow works:
|
||
|
||
```text
|
||
Südtor
|
||
↓
|
||
travel to Verbrannte Straße
|
||
↓
|
||
Jagd
|
||
↓
|
||
Jagd beginnen
|
||
↓
|
||
select Aschenratte or Straßenräuber
|
||
↓
|
||
Angreifen
|
||
↓
|
||
server creates Combat from HuntEncounter
|
||
↓
|
||
navigate to /combat/:combatId
|
||
↓
|
||
player and monster appear
|
||
↓
|
||
both HP bars visible
|
||
↓
|
||
Runde 1 visible
|
||
↓
|
||
player clicks Angriff
|
||
↓
|
||
server resolves complete round
|
||
↓
|
||
combat events appear
|
||
↓
|
||
HP changes
|
||
↓
|
||
continue attacking
|
||
↓
|
||
combat reaches WON or LOST
|
||
↓
|
||
finished state displayed
|
||
```
|
||
|
||
---
|
||
|
||
# 51. Required verification
|
||
|
||
Before considering the slice complete, run all relevant:
|
||
|
||
```text
|
||
API unit tests
|
||
API integration tests
|
||
Angular tests
|
||
API build
|
||
Angular build
|
||
migration compilation
|
||
migration execution
|
||
seed verification
|
||
```
|
||
|
||
Perform browser walkthroughs for at least:
|
||
|
||
```text
|
||
Aschenratte combat
|
||
Straßenräuber combat
|
||
victory
|
||
defeat or deterministic forced-loss test setup
|
||
browser refresh during ACTIVE combat
|
||
browser refresh after finished combat
|
||
reusing consumed HuntEncounter
|
||
attempting second combat while one is ACTIVE
|
||
```
|
||
|
||
---
|
||
|
||
# 52. Acceptance criteria
|
||
|
||
The implementation is accepted when:
|
||
|
||
- Combat starts only from persisted HuntEncounter IDs
|
||
- one encounter cannot create multiple combats
|
||
- one character cannot have multiple active combats
|
||
- combat state is stored in PostgreSQL
|
||
- player and monster HP are server-owned
|
||
- ATTACK is resolved server-side
|
||
- damage follows the defined formula
|
||
- minimum damage is 1
|
||
- monster attacks only if still alive
|
||
- combat progresses round by round
|
||
- CombatEvents are persisted
|
||
- events are ordered
|
||
- combat can reach WON
|
||
- combat can reach LOST
|
||
- finished combat rejects further actions
|
||
- page refresh restores combat state
|
||
- Angular never calculates authoritative damage
|
||
- Angular displays player and monster visually
|
||
- HP bars, round and combat log work
|
||
- combat visually matches the existing Ashen Realms UI
|
||
- no rewards are granted yet
|
||
- all relevant tests pass
|
||
- frontend and backend builds succeed
|
||
|
||
---
|
||
|
||
# 53. Handoff to Playable Slice 0.4
|
||
|
||
The exact output boundary of this slice is:
|
||
|
||
```text
|
||
Combat.status = WON
|
||
```
|
||
|
||
with a persisted finished combat.
|
||
|
||
Playable Slice 0.4 will begin from that state and implement:
|
||
|
||
```text
|
||
WON Combat
|
||
↓
|
||
reward resolution
|
||
↓
|
||
XP
|
||
↓
|
||
silver
|
||
↓
|
||
loot roll
|
||
↓
|
||
reward persistence
|
||
↓
|
||
loot summary
|
||
```
|
||
|
||
The combat system must therefore finish a fight cleanly without granting rewards itself.
|
||
|
||
Reward calculation belongs to the next slice.
|
||
|
||
---
|
||
|
||
# 54. Architectural boundary
|
||
|
||
The final separation should be:
|
||
|
||
```text
|
||
HuntingService
|
||
↓
|
||
valid HuntEncounter
|
||
|
||
CombatService
|
||
↓
|
||
persistent combat orchestration
|
||
|
||
CombatEngineService
|
||
↓
|
||
pure deterministic round resolution
|
||
|
||
Slice 0.4
|
||
↓
|
||
reward / loot processing
|
||
```
|
||
|
||
Do not let these responsibilities collapse into a single service.
|
||
|
||
The combat engine resolves combat.
|
||
|
||
It does not know about:
|
||
|
||
```text
|
||
TypeORM
|
||
loot tables
|
||
inventory
|
||
XP progression
|
||
shops
|
||
quests
|
||
Angular
|
||
```
|
||
|
||
---
|
||
|
||
# 55. Summary
|
||
|
||
Playable Slice 0.3 proves the second major gameplay pillar of Ashen Realms:
|
||
|
||
> **A server-generated encounter becomes a real visual, deterministic, persistent turn-based fight.**
|
||
|
||
The player can now:
|
||
|
||
```text
|
||
travel
|
||
→ hunt
|
||
→ choose an enemy
|
||
→ fight
|
||
→ win or lose
|
||
```
|
||
|
||
The next slice adds the reason to keep doing it:
|
||
|
||
```text
|
||
loot and progression
|
||
``` |