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

1918 lines
25 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.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
```