Reorders the Verbrannte Straße POIs to plan §8's authored sequence
(hunt, investigate, search, then the scout) — purely a keyboard tab-order
fix, since hotspots are placed by percentage, not list order.
Rewrites the location page's viewport-height reserve as a calc() over the
same rem values the top bar and footer already declare as their own
min-block-size, with file:line pointers to both, instead of an opaque
191px constant. Doesn't remove the underlying coupling (still no
ResizeObserver / shared token), but a future edit to either component's
minimum height now has a documented, unit-matching term to update instead
of an unexplained magic number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in the encounter-status feature (cleared/resumed hunt encounters)
and its own independent Verwilderter Straßenhund / Verkohlter Plünderer
assets. Both branches added the same two monsters at the same time;
resolved by keeping master's asset set as canonical (images/combat/icons/,
images/combat/sprites/) rather than maintaining a parallel copy under
images/monsters/icons/ — dropped that directory and pointed the seed's
MonsterDefinition.iconPath, the local-view test fixtures and the frontend
icon lookup at the existing combat/icons paths instead. Kept this
branch's COMBAT_MONSTER_SCALE entries for the two monsters, since master
never added them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shell sizes itself with min-block-size everywhere, which is a floor,
not a ceiling. Against that indefinite ancestor, the location page's
minmax(0, 1fr) scene row fell back to content-based sizing instead of
being bounded, so the artwork could grow tall enough to push the action
bar off screen — confirmed visually at 1920 and 1024px widths, where the
bar was fully or partially clipped.
Gives the location page its own definite, viewport-bounded height
(reserve = stable top bar + footer + own padding) instead of touching the
shared shell, which other screens still size freely. The narrow/tablet
breakpoint had a second instance of the same class of bug: the sidebar's
auto-sized row claimed its full content height before the 1fr main row
saw any space at all, collapsing the action bar to 0px height. Swapping
which row is auto vs. 1fr — main first — fixes it the same way.
Also re-anchors the four Verbrannte Straße hotspots to painted detail in
the real artwork (cart, roadside grave, road, cracked stones) rather than
the composition-reference coordinates, and lets primary-action labels
stay on one line via clamp() instead of wrapping unevenly across widths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A finished journey now opens the location view instead of leaving the
player on the map, and backing out of the hunt returns to the place the
hunt happens in. The victory and defeat screens gain "Zum Ort" alongside
"Weiter jagen", so the location is always reachable without costing the
hunt loop its one-click rhythm.
The store raises the arrival only after the server-owned current location
has been re-read, and does not navigate itself — timers, arrival times and
the server-side completion are untouched; only the screen that shows the
result changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The screen a player stands on between activities: name, region, scene
artwork with hotspots pinned by percentage, a four-button action bar and
a context sidebar covering identity, danger, encounters, interactions and
rewards.
It owns no knowledge of any particular place. Hotspots and actions are
routed by interaction type: HUNT and MAP hand off to the existing hunt
and map screens, and everything that reveals text goes through the
server-authoritative interaction endpoint. A second location therefore
renders by supplying different content, which the Südtor case in the page
spec exercises.
The shell drops its generic area rail on /location, where the screen's
own sidebar says the same thing better, and Ort joins the navigation as
its first entry. Root and unknown routes now land on the location rather
than the map: arriving somewhere should mean arriving at a place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in the First Loot slice. Resolved additively:
- MonsterDefinition keeps both the new iconPath and master's lootTableId.
- The seed keeps the four-monster pool and the local view content, and
gives the two new monsters existing loot tables — the road dog shares
the beast table, the charred looter the raider table.
- The local location view migration moves to 1788700000000 so it orders
deterministically after the loot migration, which claimed the same
timestamp.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hunt screen kept whatever roll was last in memory, so a player coming
back from a fight saw every encounter as fresh. Encounters now carry their
own status, which the combat module advances as fights start and end.
- hunt_encounters.status replaces consumed_at, which only recorded that a
fight had begun and could not distinguish a win from a loss
- a lost fight hands the encounter back as AVAILABLE, so it can be retried;
the unique index tying one combat to one encounter goes with it
- GET /hunts/active serves the resumable hunt, which the hunt page adopts on
entry rather than trusting its in-memory roll
- defeated encounters are crossed out and lose their hover and attack action
- a fresh page load rejoins a combat the server still holds open
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirrors the extended current-location payload and the interaction endpoint
in the web API client. Replaces the per-spec CurrentLocationResponse
literals with one shared fixture factory, so a contract change is answered
in a single place. Adds the drawn glyph set used by hotspots, actions and
reward previews.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the server side of the local location view: location_definitions
carries region naming, a location type, a scene-level description and
artwork, plus JSONB points of interest, primary actions and a reward
preview. Locations become content, so a second location renders through
the same components with different data.
GET /api/world/current-location gains those fields, a recommendation
label and a danger rating derived from the weighted average of the
location's own monster pool — a rare elite no longer makes a beginner
road read as lethal. The encounter preview is derived from that same
pool rather than duplicating it.
POST /api/world/current-location/interactions/:key reveals a hotspot's
authored result. The location is resolved from the character, never from
the request, and result text never ships with the location payload, so a
caller cannot read or trigger a hotspot it has not travelled to.
Seeds the Verbrannte Straße with its four hotspots and the Südtor with
its own transition content. Adds Verwilderter Straßenhund and Verkohlter
Plünderer to the road's pool, including combat sprites, so the preview
shows encounters the hunt can actually roll. Medallion icons move to
images/monsters/icons, where both combat and the location view read them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a real-SQL-inspection test for CreateLootAndRewards1788600000000
(mirroring the visible-vertical-slice pattern) so the one-reward-per-combat
unique index and other DB-level invariants can't be silently deleted
without failing a test, and asserts the TopBar renders character.silver
and character.experience values already present in the app.spec.ts fixture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server resolves a whole round in one call, so both blows used to land
at the same instant. The page now keeps its own view of the combat and
plays the round back: the swing animates, the monster's HP and log line
land, then after a beat the monster strikes and the player recoils.
Both animations are six-frame sprite sheets driven by steps(6), which is
why the phase durations mirror the stylesheet.
The status row reflows on the stage's own width via a container query —
the side rails can squeeze it narrow while the viewport is still wide,
which previously overlapped the round marker with the player's name. The
component-style budget moves to 12kB to fit this screen's stylesheet.
Swap the enemy body art for the background-free versions and trim every
sprite to its opaque bounds so the fighters share one ground baseline
instead of floating in a padded box.
Sprite height is now a share of the battlefield rather than a fixed
clamp, set per monster key, so a low-slung rat and a standing bandit keep
believable proportions against the player at any stage size.
Attacking while a combat is already active returned COMBAT_ALREADY_ACTIVE
and left the hunt page showing an error the player could not act on, with
no way back into the fight they were already in.
Add GET /api/combats/active so the client can resolve that combat, and
have the hunt page navigate into it when an attack is rejected for this
reason. CombatStore now also exposes the error code so callers can tell
this case apart from a genuinely failed attack.
Move both health bars to a status row at the top of the scene with a
circular portrait beside each, and place full-body sprites for the
player and the monster standing on the location background instead of
square portraits. The Angriff action now uses the ornate HUD frame art,
with the keybind in the frame's own tab.
Sprite and icon derivatives are keyed by monster key so both seeded
monsters resolve; the Aschenratte body art still carries its original
backdrop until a cut-out version replaces it at the same path.
Wires up the Slice 0.3 combat screen (player/monster HP bars, round
display, Angriff action, grouped German combat log, victory/defeat
panels) and replaces the Slice 0.2 combat/new placeholder route with
combat/:combatId loading CombatPageComponent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signal-based store wrapping GameApiService.startCombat/getCombat/
performCombatAction with loading/error/actionPending state, following
the HuntingStore/WorldStore pattern. startCombat failures clear any
previously-loaded combat; loadCombat/attack failures preserve the
last-known-good combat. attack() guards against re-entrancy and
no-loaded-combat calls.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Combat domain models (Combat, CombatPlayer, CombatMonster, CombatEvent)
and three GameApiService methods to interact with backend combat endpoints:
- startCombat(encounterId): POST /api/hunt-encounters/:id/attack
- getCombat(combatId): GET /api/combats/:id
- performCombatAction(combatId, action): POST /api/combats/:id/actions
Includes test coverage for all three methods.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
encounter-card.component rendered the raw 2.39MB ash-rat.png and
2.00MB road-bandit.png directly, up to 3 cards per hunt (~7MB/load,
re-rendered on "Neu suchen"). Add 560px-wide JPEG runtime derivatives
(generated via PowerShell System.Drawing, HighQualityBicubic, quality
82) and switch to the <picture>/<source srcset> pattern already used
by context-panel for location backgrounds, keeping the original PNGs
as the <img> fallback. Also add loading="lazy" decoding="async".
ash-rat.png: 2,506,677 B -> ash-rat-560.jpg: 35,896 B
road-bandit.png: 2,097,049 B -> road-bandit-560.jpg: 50,797 B
HuntPageComponent never called WorldStore.load(), so opening /hunt
directly (bookmark/hard refresh) without first visiting /world left
currentLocation() at null forever, stranding the page and the context
panel on their empty states with no recovery. Add ngOnInit that calls
worldStore.load() only when no location is present yet, mirroring
WorldPageComponent's existing call and avoiding a duplicate request.
Wires up Slice 0.2 end-to-end: HuntPageComponent renders the
hunting-unavailable/ready/loading/encounters-found states off
HuntingStore and WorldStore, Angreifen hands the HuntEncounter id to a
new inert CombatPlaceholderPageComponent via /combat/new, the Jagd nav
entry is enabled with router-driven active state (matching Karte's),
and the context panel now lists possible encounters for hunting-enabled
locations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Code review flagged .encounter-card__artwork-frame using a new
hardcoded #050607 background instead of reusing an existing design
token, violating the no-new-hex-colors requirement.
Renders the 5 HuntEncounter danger tiers as German text plus a
per-tier BEM modifier class, mirroring travel-panel's text+class
danger pattern so color is never the sole signal. Colors map across
the existing 3 semantic tokens (success/warning/danger).
Mirrors WorldStore's signal-store architecture to own hunt state for
Slice 0.2 (currentHunt, selectedEncounterId, loading, error) with a
computed encounters accessor and the four hunt error codes mapped to
German messages.
Add to game-api.models.ts:
- DangerRating type for hunt encounter danger levels
- MonsterSummary interface with key, name, level, artworkPath
- HuntEncounter interface for individual hunt encounters
- HuntResult interface for hunt completion results, reusing LocationSummary
Extend CurrentLocationResponse with possibleMonsters: string[] field.
Add to game-api.service.ts:
- startHunt(): Observable<HuntResult> method posting to /api/hunts
Update test fixtures to include possibleMonsters field in mock locations.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Adds the two starter monster definitions and their location-monster
mapping (weights 70/30) to the vertical-slice seed, following the
same findOneBy/update-or-insert pattern used for locations, plus an
upsert on (locationId, monsterId) for the mapping. Copies the source
artwork into the served images/monsters directory and extends the
seed spec harness to cover both idempotent inserts.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
HttpErrorResponse implements the Error interface structurally but does not
extend the Error class, so `error instanceof Error` was always false for
HTTP failures and every travel error (400/409/500/network) fell through to
the same generic fallback string, hiding the backend's specific
TravelDomainError code/message (e.g. TRAVEL_ALREADY_ACTIVE) from the user.
toErrorMessage now checks `error instanceof HttpErrorResponse` first and
maps known travel error codes to specific German messages, falling back to
the generic message for unknown codes and to `error.message` for genuine
non-HTTP errors.
startTravel()'s catch block also now resyncs current travel state from the
server (reusing the existing pollCurrentTravel/refreshCurrentTravel path)
so a failed start caused by a race with another tab/request doesn't leave
the store's local state stale.
apps/web/public/images was shipping ~90 MB of source art (enemies, npc,
combat status icons, HUD icon originals/difficulty badges, unused
background paintings) that no component, template, or stylesheet
actually references, since Angular copies public/ verbatim into every
browser build. Moved everything not referenced under apps/web/src to a
new art/ directory at the repo root, preserving the original subfolder
layout; only the 11 files actually loaded by the app (runtime HUD/
background derivatives and their PNG fallbacks) remain under
apps/web/public/images. Documented the split in README.md.
Drives the full world/travel flow in Chromium via Playwright against the
live API and seeded Postgres, capturing the three required desktop
viewports and comparing them against the accepted design reference.
Fixes two evidenced defects found during verification:
- world-page scene sizing overflowed the viewport at all three required
desktop sizes, pushing the footer (and, at 1440x900/1366x768, the travel
button) below the fold; scene height now accounts for surrounding chrome
so nothing is clipped.
- the generic HTTP-failure fallback message was hardcoded in English while
the rest of the UI is German; now uses a matching German string.
Adds Playwright as a web devDependency for this and future browser
verification passes (no product-code dependency).