1803 lines
26 KiB
Markdown
1803 lines
26 KiB
Markdown
# 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<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 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=<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
|
||
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. |