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

1803 lines
26 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.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 23 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 23 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: 47
```
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: 915
```
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<HuntResultDto>
```
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 23 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
23 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
23 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:
13
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=<uuid>
```
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<HuntResponseDto>
```
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
23 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 23 HuntEncounter records
Angular renders 23 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
- 23 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.