# 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 getCombat( combatId: string, ): Observable performAction( combatId: string, action: CombatAction, ): Observable ``` 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 ```