# Ashen Realms – Playable Slice 0.2: First Hunt **Status:** Ready for implementation **Scope:** Hunting / Encounter Selection **Prerequisite:** Playable Slice 0.1 – World & Travel is implemented and working **Primary Location:** Verbrannte Straße **Primary Goal:** The player can start a hunt at the Verbrannte Straße, receive 2–3 server-generated encounters, view them in the Angular UI, refresh the hunt, and select one encounter for the future combat flow. --- # 1. Goal Implement the next playable step of the Ashen Realms core loop: ```text Travel → arrive at Verbrannte Straße → start hunt → receive encounters → inspect enemies → choose an enemy ``` This slice must prove that: - hunting is tied to the current authoritative location - encounter generation happens only on the server - available monsters are data-driven - the player cannot freely request arbitrary monster IDs - the frontend presents encounters visually instead of as a plain list - the existing Ashen Realms application shell and visual language remain consistent This slice deliberately stops before actual combat resolution. The next slice will be: ```text Playable Slice 0.3 – First Combat ``` --- # 2. Non-goals Do **not** implement the following in this slice: - combat engine - combat persistence - combat actions - damage calculation - loot - XP rewards - silver rewards - inventory - equipment - item drops - quests - merchants - area currencies - bosses - elites beyond data preparation - authentication - random travel ambushes - advanced status effects - timers for hunting - real-time communication - WebSockets - background jobs Do not expand the architecture for systems that are not required by this slice. --- # 3. Existing assumptions The project already contains: ```text Angular frontend NestJS backend PostgreSQL TypeORM npm workspace monorepo ``` Existing gameplay systems already provide: ```text Character LocationDefinition LocationConnection Travel current character location world screen travel between Südtor and Verbrannte Straße server-authoritative travel completion ``` The existing application shell must remain unchanged: ```text TopBar SideNavigation Main Content Context Panel Footer ``` The new hunting UI must integrate into this shell. --- # 4. Core gameplay flow The intended player flow is: ```text Player travels to Verbrannte Straße ↓ Travel completes on the server ↓ Player opens "Jagd" ↓ Frontend loads current location ↓ Player clicks "Jagd beginnen" ↓ POST /api/hunts ↓ Server verifies that hunting is allowed ↓ Server loads monster pool for current location ↓ Server selects 2–3 encounters ↓ Hunt and HuntEncounter records are persisted ↓ Frontend displays encounter cards ↓ Player may: choose an encounter or click "Neu suchen" ``` Selecting an encounter does not yet start real combat. The selected `HuntEncounter.id` must already be usable by the future combat slice. --- # 5. Domain rules ## 5.1 Hunting availability Hunting is only allowed when: ```text character.currentLocation.huntingEnabled === true ``` For the current slice: ```text Südtor von Graufurt huntingEnabled = false Verbrannte Straße huntingEnabled = true ``` If hunting is started at a location where hunting is disabled, the backend must reject the request. Example domain error: ```json { "statusCode": 400, "code": "HUNTING_NOT_AVAILABLE", "message": "Hunting is not available at the current location." } ``` --- # 6. Monster content model Introduce a persistent content definition for monsters. ## 6.1 MonsterDefinition Create: ```text apps/api/src/monsters/entities/monster-definition.entity.ts ``` Required fields: ```ts id: uuid key: string name: string level: number maxHp: number attack: number armor: number experienceReward: number silverMin: number silverMax: number artworkPath: string createdAt: timestamptz updatedAt: timestamptz ``` Constraints: ```text key must be unique ``` Examples: ```text ash-rat road-bandit ``` The backend must use `key` for stable content references. UUIDs remain persistence identifiers. --- # 7. Location monster pool Create the relationship between a location and its possible monsters. ## 7.1 LocationMonster Create: ```text apps/api/src/monsters/entities/location-monster.entity.ts ``` Fields: ```ts id: uuid locationId: uuid monsterId: uuid weight: number encounterType: string enabled: boolean ``` Recommended initial encounter types: ```text NORMAL RARE ELITE BOSS ``` For this slice only: ```text NORMAL ``` is required. The relationship must allow the same monster definition to appear at multiple locations later. --- # 8. Initial monster content Seed exactly two monsters for this slice. ## 8.1 Aschenratte Stable key: ```text ash-rat ``` Display name: ```text Aschenratte ``` Values: ```text Level: 1 XP reward: 8 Silver: 4–7 ``` Combat values may use provisional deterministic numbers if they are not already defined elsewhere. They are not used for actual combat in this slice. Recommended placeholder values: ```text maxHp: 45 attack: 5 armor: 0 ``` If existing project content already defines different values, preserve the existing values instead. Artwork: ```text /assets/monsters/ash-rat.webp ``` or use the actual existing asset path in the repository. Do not invent a duplicate asset if an appropriate one already exists. --- # 9. Straßenräuber Stable key: ```text road-bandit ``` Display name: ```text Straßenräuber ``` Values: ```text Level: 2 XP reward: 16 Silver: 9–15 ``` Recommended provisional combat values: ```text maxHp: 75 attack: 9 armor: 5 ``` Again: If the repository already contains finalized values, use those instead. Artwork: ```text /assets/monsters/road-bandit.webp ``` or the actual existing asset path. --- # 10. Location pool for Verbrannte Straße Both monsters must be assigned to: ```text burned-road ``` Example: ```text Aschenratte weight = 70 Straßenräuber weight = 30 ``` The exact weight numbers are implementation values for this technical slice and may later be rebalanced. The system must not hardcode monster selection based on location keys inside the HuntingService. The service must load the pool from persisted `LocationMonster` records. --- # 11. Hunt persistence Create: ```text apps/api/src/hunting/entities/hunt.entity.ts ``` ## 11.1 Hunt Fields: ```ts id: uuid characterId: uuid locationId: uuid status: enum createdAt: timestamptz ``` For this slice the status may contain: ```text ACTIVE SUPERSEDED ``` Optional: ```text SELECTED ``` if useful for the later combat handoff. Keep the state model small. --- # 12. HuntEncounter persistence Create: ```text apps/api/src/hunting/entities/hunt-encounter.entity.ts ``` Fields: ```ts id: uuid huntId: uuid monsterDefinitionId: uuid position: number createdAt: timestamptz ``` Recommended positions: ```text 0 1 2 ``` The encounter must persist the monster that was rolled. A later combat flow must reference: ```text HuntEncounter.id ``` and not accept arbitrary `MonsterDefinition.id` values from the frontend. This is a critical server-authority requirement. --- # 13. Hunting module Create: ```text apps/api/src/hunting/ ``` Recommended structure: ```text hunting/ ├── dto/ │ └── hunt-response.dto.ts ├── entities/ │ ├── hunt.entity.ts │ └── hunt-encounter.entity.ts ├── hunting.controller.ts ├── hunting.module.ts ├── hunting.service.ts └── hunting.service.spec.ts ``` --- # 14. HuntingService Required service: ```ts class HuntingService ``` Primary method: ```ts startHunt(characterId: string): Promise ``` Responsibilities: ```text load character ↓ resolve authoritative current location ↓ verify no active travel prevents interaction ↓ verify location exists ↓ verify huntingEnabled ↓ load enabled LocationMonster records ↓ validate that pool is not empty ↓ select 2–3 encounters ↓ persist Hunt ↓ persist HuntEncounter records ↓ return DTO ``` The entire operation should use a transaction where appropriate. --- # 15. Travel interaction A player who is currently travelling must not be able to hunt. The backend must remain authoritative. If an active travel exists: ```json { "statusCode": 409, "code": "CHARACTER_TRAVELLING", "message": "The character cannot hunt while travelling." } ``` Do not rely on Angular to enforce this rule. --- # 16. Empty monster pool If hunting is enabled but no enabled monster definitions exist for the location, return a clear server error. Example: ```json { "statusCode": 409, "code": "NO_HUNT_ENCOUNTERS_AVAILABLE", "message": "No encounters are currently available at this location." } ``` Do not silently return an empty hunt. --- # 17. Encounter count The desired gameplay rule is: ```text 2–3 encounters ``` For the first implementation, the server may choose: ```text 3 encounters by default ``` if the pool contains enough valid entries. Duplicate monster types are allowed. Example: ```text Aschenratte Straßenräuber Aschenratte ``` This is acceptable. Each encounter still receives its own unique `HuntEncounter.id`. --- # 18. Weighted random selection Encounter selection must be server-side and based on `LocationMonster.weight`. Do not use: ```ts Math.random() ``` directly throughout business logic. Introduce a small injectable random source. Example: ```ts export interface RandomSource { next(): number; } ``` Production implementation: ```ts next(): number { return Math.random(); } ``` Tests must inject deterministic values. This is required so encounter selection tests remain reproducible. --- # 19. Weighted selection behavior Conceptually: ```text Aschenratte weight 70 Straßenräuber weight 30 ``` means the rat is more likely to appear. Selection may be performed independently for each encounter slot. No duplicate prevention is required for this slice. --- # 20. API Create: ```http POST /api/hunts ``` No request body is required. The server already knows the current demo character. Do not send: ```json { "locationId": "...", "monsterId": "..." } ``` The client must not determine the hunting location or monster pool. The server derives both from the current character state. --- # 21. Hunt response DTO Example response: ```json { "id": "hunt-uuid", "location": { "id": "location-uuid", "key": "burned-road", "name": "Verbrannte Straße" }, "encounters": [ { "id": "encounter-uuid-1", "monster": { "key": "ash-rat", "name": "Aschenratte", "level": 1, "artworkPath": "/assets/monsters/ash-rat.webp" }, "dangerRating": "MATCH" }, { "id": "encounter-uuid-2", "monster": { "key": "road-bandit", "name": "Straßenräuber", "level": 2, "artworkPath": "/assets/monsters/road-bandit.webp" }, "dangerRating": "STRONG" }, { "id": "encounter-uuid-3", "monster": { "key": "ash-rat", "name": "Aschenratte", "level": 1, "artworkPath": "/assets/monsters/ash-rat.webp" }, "dangerRating": "MATCH" } ] } ``` Do not expose: ```text monster attack monster armor monster maxHp loot tables drop chances internal weights raw Combat Power ``` through the hunt overview unless required later. --- # 22. Danger rating Ashen Realms uses these visible ratings: ```text WEAK MATCH STRONG VERY_DANGEROUS DEADLY ``` For this slice, danger rating must still be determined by the backend. Do not calculate it in Angular. If the full `CharacterStatsService` already exists, use the existing combat-power logic. If not, create a deliberately small temporary server-side calculation. For this slice it is acceptable to return sensible values for the demo character: ```text Aschenratte → MATCH Straßenräuber → STRONG ``` Do not implement the full equipment progression system solely for this feature. The API contract should already use the final enum. --- # 23. Refreshing the hunt The player must be able to click: ```text Neu suchen ``` This should call: ```http POST /api/hunts ``` again. The previous hunt may be marked: ```text SUPERSEDED ``` before the new hunt becomes active. Do not delete historical hunt data. Only one current active hunt should exist per character. Enforce this through service logic and, if practical, a database constraint. --- # 24. Frontend route Create or activate: ```text /hunt ``` The navigation entry: ```text Jagd ``` must become enabled. Karte remains available. Other unfinished routes should remain disabled or unchanged. --- # 25. Hunt page behavior Create a hunting feature under: ```text apps/web/src/app/features/hunting/ ``` Recommended structure: ```text hunting/ ├── hunt-page/ ├── encounter-card/ ├── hunting-api.service.ts └── hunting.store.ts ``` Adapt this to the existing frontend architecture if equivalent abstractions already exist. Do not introduce a second competing state-management pattern. --- # 26. Hunt page initial state When opening `/hunt`, the frontend must load enough data to know: ```text current location whether hunting is available whether character is travelling ``` Reuse existing world/character state where appropriate. Avoid duplicate API requests if an existing store already owns authoritative location data. --- # 27. Location without hunting If the player opens `/hunt` while at Südtor: Display an in-world empty state. Example: ```text Keine Jagd verfügbar Am Südtor von Graufurt gibt es keine regulären Jagdgebiete. Reise in ein gefährlicheres Gebiet, um nach Gegnern zu suchen. ``` Primary action: ```text Zur Karte ``` Do not show an enabled "Jagd beginnen" button. --- # 28. Hunt start state At Verbrannte Straße, before a hunt has been started: The main content should show: ```text location artwork / thematic background location name short hunting description primary button: Jagd beginnen ``` Do not automatically start a hunt merely by opening `/hunt`. The player should explicitly trigger the action. --- # 29. Encounter presentation After `POST /api/hunts`, display the returned encounters as large visual cards. Each card must contain: ```text monster artwork monster name level danger rating short descriptive label if already available Angreifen button ``` The monster artwork is the visual focus. Do not render the hunt as: ```text plain HTML table compact admin list generic Material cards Bootstrap dashboard tiles ``` --- # 30. Encounter card layout Desktop target: ```text 2–3 large cards in one row ``` Each card should provide enough artwork space that the monster is visually prominent. Example structure: ```text ┌──────────────────────┐ │ │ │ Monster Art │ │ │ ├──────────────────────┤ │ Aschenratte │ │ Stufe 1 │ │ Passend │ │ │ │ [ Angreifen ] │ └──────────────────────┘ ``` The visual language must match the existing Ashen Realms UI. --- # 31. Danger badge Create or reuse: ```text DangerBadgeComponent ``` Supported states: ```text Schwach Passend Stark Sehr gefährlich Tödlich ``` Color must not be the only communication method. Always include readable text. Use the existing design token system. Do not hardcode independent colors inside the hunt component if equivalent global tokens already exist. --- # 32. Right context panel While on the hunt screen, the right-side context panel should show: ```text Verbrannte Straße Gebiet: Aschenfelder Empfohlene Stufe: 1–3 Gefahr: Niedrig Mögliche Begegnungen: Aschenratte Straßenräuber ``` Do not expose exact encounter probabilities. Do not expose hidden weights. The context panel should complement the encounter cards rather than repeat every detail. --- # 33. Hunt actions Required actions: ```text Jagd beginnen Neu suchen Angreifen Zur Karte ``` For this slice: ### Jagd beginnen Creates first hunt. ### Neu suchen Creates another hunt. ### Angreifen Selects the encounter for future combat. ### Zur Karte Navigates back to: ```text /world ``` --- # 34. Angreifen behavior in this slice Combat does not exist yet. Therefore the button must **not** create fake client-side combat. When clicked: Store or route with: ```text HuntEncounter.id ``` Recommended implementation: ```text navigate to: /combat/new?encounterId= ``` or prepare an equivalent route consistent with the application. If `/combat` is not yet implemented, display a clearly intentional placeholder state such as: ```text Kampf wird im nächsten Playable Slice implementiert. ``` However: Do not build a fake combat screen. Do not generate damage. Do not mark the monster as defeated. Do not grant loot. The encounter ID must remain intact for Slice 0.3. --- # 35. Loading states While starting or refreshing a hunt: - disable hunting action buttons - keep the current layout stable - show an in-world loading state Example: ```text Du suchst nach Spuren... ``` Do not use browser alerts. Do not blank the entire application shell. --- # 36. Error handling Errors must appear inside the Ashen Realms UI. Relevant errors include: ```text HUNTING_NOT_AVAILABLE CHARACTER_TRAVELLING NO_HUNT_ENCOUNTERS_AVAILABLE NETWORK_ERROR ``` The player should have an appropriate retry action when possible. Example: ```text Die Jagd konnte nicht gestartet werden. [ Erneut versuchen ] ``` --- # 37. Frontend state Recommended state: ```ts currentLocation currentHunt encounters loading error selectedEncounter ``` Use Angular signals if the existing world feature already uses signals. Do not introduce NgRx solely for this slice. Do not calculate random encounters in Angular. Do not calculate danger ratings in Angular. --- # 38. API client Add a focused method to the existing typed API layer. Example: ```ts startHunt(): Observable ``` Request: ```http POST /api/hunts ``` with either: ```ts {} ``` or no meaningful request payload. Do not send: ```text characterId locationId monsterId dangerRating weights ``` from the client. --- # 39. Database migration Create a TypeORM migration for: ```text monster_definition location_monster hunt hunt_encounter ``` The migration must: - use UUID primary keys - create foreign keys - create required indexes - create unique key for MonsterDefinition.key - preserve existing world/travel data - not drop or recreate unrelated tables - not use `synchronize` Review generated SQL before accepting it. --- # 40. Seed behavior Extend the existing idempotent seed. Seed: ```text ash-rat road-bandit ``` Assign both to: ```text burned-road ``` The seed must remain idempotent. Running it multiple times must: ```text not duplicate monsters not duplicate location-monster relationships not reset character location not delete travel history not delete hunt history ``` If values change later, the seed may update definition fields. --- # 41. Backend tests Implement tests before or alongside production code. At minimum cover: ## Hunting availability ```text reject hunt at south-gate ``` Expected: ```text HUNTING_NOT_AVAILABLE ``` --- ## Valid hunt Character at: ```text burned-road ``` Expected: ```text hunt created 2–3 encounters returned ``` --- ## Empty pool Location hunting enabled but no enabled monsters. Expected: ```text NO_HUNT_ENCOUNTERS_AVAILABLE ``` --- ## Travelling character Character has an active travel. Expected: ```text CHARACTER_TRAVELLING ``` --- ## Weighted random selection Inject deterministic random source. Given: ```text ash-rat weight 70 road-bandit weight 30 ``` verify predictable selection for fixed random values. Do not write probabilistic flaky tests. --- ## Persistence Verify: ```text Hunt is persisted HuntEncounter rows are persisted each encounter has unique ID monster relationship is correct ``` --- ## Refresh hunt Start first hunt. Start second hunt. Verify: ```text first hunt = SUPERSEDED second hunt = ACTIVE ``` --- # 42. Frontend tests At minimum cover: ## Hunt page at unsupported location Given: ```text south-gate ``` Expected: ```text empty state no active hunt button link to map ``` --- ## Start hunt Click: ```text Jagd beginnen ``` Verify: ```text POST /api/hunts ``` is called. --- ## Encounter rendering Given API response with: ```text Aschenratte Straßenräuber Aschenratte ``` verify: ```text 3 cards rendered names visible levels visible danger labels visible artwork paths used ``` --- ## Refresh Click: ```text Neu suchen ``` Verify another: ```text POST /api/hunts ``` occurs. --- ## Encounter selection Click: ```text Angreifen ``` Verify only: ```text HuntEncounter.id ``` is used for navigation / future combat handoff. The frontend must not send: ```text monsterId damage monster HP loot request ``` --- # 43. Visual requirements The hunt screen must follow the established Ashen Realms visual language: ```text dark fantasy large illustrated content dark metal / stone panels thin bronze details restrained blue and gold accents serif display headings readable UI text ``` Avoid: ```text white cards glassmorphism SaaS layout generic dashboard cards Material default styles Bootstrap look bright neon effects ``` The UI must feel like part of the same application as the world map. --- # 44. Accessibility Encounter cards and actions must be keyboard accessible. Required: ```text visible focus state real buttons readable danger text meaningful alt text for artwork where appropriate disabled state during loading ``` Do not encode danger only by color. Support: ```css prefers-reduced-motion ``` for decorative motion. --- # 45. Performance Do not preload unnecessary future gameplay systems. The hunt page should load only: ```text current required state encounter artwork hunt API response ``` Reuse existing image-loading patterns. Avoid introducing large dependencies. --- # 46. Server authority checklist The backend must decide: ```text whether hunting is allowed which location is used which monsters are available which encounters are rolled how encounter weights work which danger rating is returned which HuntEncounter IDs exist ``` The frontend may decide only: ```text when the player requests a hunt which returned encounter the player clicks ``` The frontend must never be authoritative over gameplay state. --- # 47. Definition of Done Playable Slice 0.2 is complete when the following manual flow works: ```text Start application ↓ Character is at Südtor ↓ Open Jagd ↓ UI explains that hunting is unavailable ↓ Open Karte ↓ Travel to Verbrannte Straße ↓ Server completes travel ↓ Open Jagd ↓ Click "Jagd beginnen" ↓ Server creates Hunt ↓ Server creates 2–3 HuntEncounter records ↓ Angular renders 2–3 large encounter cards ↓ Cards show monster artwork, name, level and danger ↓ Click "Neu suchen" ↓ New server-generated hunt appears ↓ Click "Angreifen" on one encounter ↓ The selected HuntEncounter.id is preserved for the next combat slice ``` --- # 48. Required verification Before considering the slice complete, run: ```text API unit tests API integration tests Angular tests API build Angular build TypeORM migration compilation seed execution ``` Perform a browser walkthrough at minimum for: ```text Südtor → Jagd unavailable Südtor → Verbrannte Straße travel Verbrannte Straße → Jagd first hunt refresh hunt encounter selection ``` --- # 49. Explicit implementation constraints Do not: ```text implement combat implement loot implement inventory implement equipment implement XP progression add authentication add quests add shops add WebSockets add background jobs add microservices add Redis add CQRS add event sourcing add NgRx hardcode monster arrays in Angular trust client location IDs trust client monster IDs calculate encounters client-side calculate danger client-side ``` Keep the implementation intentionally small. --- # 50. Expected repository additions Likely backend additions: ```text apps/api/src/monsters/ apps/api/src/hunting/ apps/api/src/database/migrations/ apps/api/src/database/seeds/ ``` Likely frontend additions: ```text apps/web/src/app/features/hunting/ ``` Reuse existing: ```text API client application shell design tokens world state where sensible shared components error presentation loading presentation ``` Do not duplicate working infrastructure. --- # 51. Acceptance criteria The implementation is accepted when: - `Verbrannte Straße` supports hunting - `Südtor` does not - hunting uses server-authoritative current location - encounter pools come from PostgreSQL content data - Aschenratte and Straßenräuber are seeded - 2–3 encounters are generated server-side - weighted selection is testable deterministically - Hunt and HuntEncounter are persisted - refreshing creates a new hunt - old hunt is no longer active - Angular renders large visual encounter cards - danger rating is server-provided - the hunt page matches the existing Ashen Realms design - the client cannot freely choose arbitrary monster IDs - clicking Angreifen preserves only the returned HuntEncounter ID - no combat, loot or inventory system is implemented prematurely - all relevant tests pass - API and frontend builds succeed --- # 52. Next slice After this slice is fully working, continue with: ```text Playable Slice 0.3 – First Combat ``` That slice will begin from: ```text HuntEncounter.id ``` and implement: ```text Encounter → Combat creation → player vs monster screen → turn-based actions → deterministic server-side combat → victory / defeat ``` Do not begin Slice 0.3 until Slice 0.2 is fully verified.