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

26 KiB
Raw Blame History

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:

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:

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:

Angular frontend
NestJS backend
PostgreSQL
TypeORM
npm workspace monorepo

Existing gameplay systems already provide:

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:

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:

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:

character.currentLocation.huntingEnabled === true

For the current slice:

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:

{
  "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:

apps/api/src/monsters/entities/monster-definition.entity.ts

Required fields:

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:

key must be unique

Examples:

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:

apps/api/src/monsters/entities/location-monster.entity.ts

Fields:

id: uuid

locationId: uuid
monsterId: uuid

weight: number

encounterType: string

enabled: boolean

Recommended initial encounter types:

NORMAL
RARE
ELITE
BOSS

For this slice only:

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:

ash-rat

Display name:

Aschenratte

Values:

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:

maxHp: 45
attack: 5
armor: 0

If existing project content already defines different values, preserve the existing values instead.

Artwork:

/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:

road-bandit

Display name:

Straßenräuber

Values:

Level: 2
XP reward: 16
Silver: 915

Recommended provisional combat values:

maxHp: 75
attack: 9
armor: 5

Again:

If the repository already contains finalized values, use those instead.

Artwork:

/assets/monsters/road-bandit.webp

or the actual existing asset path.


10. Location pool for Verbrannte Straße

Both monsters must be assigned to:

burned-road

Example:

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:

apps/api/src/hunting/entities/hunt.entity.ts

11.1 Hunt

Fields:

id: uuid

characterId: uuid
locationId: uuid

status: enum

createdAt: timestamptz

For this slice the status may contain:

ACTIVE
SUPERSEDED

Optional:

SELECTED

if useful for the later combat handoff.

Keep the state model small.


12. HuntEncounter persistence

Create:

apps/api/src/hunting/entities/hunt-encounter.entity.ts

Fields:

id: uuid

huntId: uuid
monsterDefinitionId: uuid

position: number

createdAt: timestamptz

Recommended positions:

0
1
2

The encounter must persist the monster that was rolled.

A later combat flow must reference:

HuntEncounter.id

and not accept arbitrary MonsterDefinition.id values from the frontend.

This is a critical server-authority requirement.


13. Hunting module

Create:

apps/api/src/hunting/

Recommended structure:

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:

class HuntingService

Primary method:

startHunt(characterId: string): Promise<HuntResultDto>

Responsibilities:

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:

{
  "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:

{
  "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:

23 encounters

For the first implementation, the server may choose:

3 encounters by default

if the pool contains enough valid entries.

Duplicate monster types are allowed.

Example:

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:

Math.random()

directly throughout business logic.

Introduce a small injectable random source.

Example:

export interface RandomSource {
  next(): number;
}

Production implementation:

next(): number {
  return Math.random();
}

Tests must inject deterministic values.

This is required so encounter selection tests remain reproducible.


19. Weighted selection behavior

Conceptually:

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:

POST /api/hunts

No request body is required.

The server already knows the current demo character.

Do not send:

{
  "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:

{
  "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:

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:

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:

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:

Neu suchen

This should call:

POST /api/hunts

again.

The previous hunt may be marked:

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:

/hunt

The navigation entry:

Jagd

must become enabled.

Karte remains available.

Other unfinished routes should remain disabled or unchanged.


25. Hunt page behavior

Create a hunting feature under:

apps/web/src/app/features/hunting/

Recommended structure:

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:

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:

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:

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:

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:

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:

plain HTML table
compact admin list
generic Material cards
Bootstrap dashboard tiles

30. Encounter card layout

Desktop target:

23 large cards in one row

Each card should provide enough artwork space that the monster is visually prominent.

Example structure:

┌──────────────────────┐
│                      │
│     Monster Art      │
│                      │
├──────────────────────┤
│ Aschenratte          │
│ Stufe 1              │
│ Passend              │
│                      │
│ [ Angreifen ]        │
└──────────────────────┘

The visual language must match the existing Ashen Realms UI.


31. Danger badge

Create or reuse:

DangerBadgeComponent

Supported states:

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:

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:

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:

/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:

HuntEncounter.id

Recommended implementation:

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:

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:

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:

HUNTING_NOT_AVAILABLE
CHARACTER_TRAVELLING
NO_HUNT_ENCOUNTERS_AVAILABLE
NETWORK_ERROR

The player should have an appropriate retry action when possible.

Example:

Die Jagd konnte nicht gestartet werden.

[ Erneut versuchen ]

37. Frontend state

Recommended state:

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:

startHunt(): Observable<HuntResponseDto>

Request:

POST /api/hunts

with either:

{}

or no meaningful request payload.

Do not send:

characterId
locationId
monsterId
dangerRating
weights

from the client.


39. Database migration

Create a TypeORM migration for:

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:

ash-rat
road-bandit

Assign both to:

burned-road

The seed must remain idempotent.

Running it multiple times must:

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

reject hunt at south-gate

Expected:

HUNTING_NOT_AVAILABLE

Valid hunt

Character at:

burned-road

Expected:

hunt created
23 encounters returned

Empty pool

Location hunting enabled but no enabled monsters.

Expected:

NO_HUNT_ENCOUNTERS_AVAILABLE

Travelling character

Character has an active travel.

Expected:

CHARACTER_TRAVELLING

Weighted random selection

Inject deterministic random source.

Given:

ash-rat weight 70
road-bandit weight 30

verify predictable selection for fixed random values.

Do not write probabilistic flaky tests.


Persistence

Verify:

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:

first hunt = SUPERSEDED
second hunt = ACTIVE

42. Frontend tests

At minimum cover:

Hunt page at unsupported location

Given:

south-gate

Expected:

empty state
no active hunt button
link to map

Start hunt

Click:

Jagd beginnen

Verify:

POST /api/hunts

is called.


Encounter rendering

Given API response with:

Aschenratte
Straßenräuber
Aschenratte

verify:

3 cards rendered
names visible
levels visible
danger labels visible
artwork paths used

Refresh

Click:

Neu suchen

Verify another:

POST /api/hunts

occurs.


Encounter selection

Click:

Angreifen

Verify only:

HuntEncounter.id

is used for navigation / future combat handoff.

The frontend must not send:

monsterId
damage
monster HP
loot request

43. Visual requirements

The hunt screen must follow the established Ashen Realms visual language:

dark fantasy
large illustrated content
dark metal / stone panels
thin bronze details
restrained blue and gold accents
serif display headings
readable UI text

Avoid:

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:

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:

prefers-reduced-motion

for decorative motion.


45. Performance

Do not preload unnecessary future gameplay systems.

The hunt page should load only:

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:

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:

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:

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:

API unit tests
API integration tests
Angular tests
API build
Angular build
TypeORM migration compilation
seed execution

Perform a browser walkthrough at minimum for:

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:

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:

apps/api/src/monsters/
apps/api/src/hunting/
apps/api/src/database/migrations/
apps/api/src/database/seeds/

Likely frontend additions:

apps/web/src/app/features/hunting/

Reuse existing:

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:

Playable Slice 0.3  First Combat

That slice will begin from:

HuntEncounter.id

and implement:

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.