Commit Graph

65 Commits

Author SHA1 Message Date
Bastian Wagner
eed247d318 feat(combat): support a damage multiplier in calculateDamage 2026-08-20 21:06:54 +02:00
Bastian Wagner
49a0008a2b Merge branch 'worktree-playable-slice-0.5-first-upgrade'
# Conflicts:
#	apps/web/src/app/core/api/game-api.service.ts
#	apps/web/src/app/features/combat/combat-page/combat-page.component.html
#	apps/web/src/app/features/combat/combat-page/combat-page.component.scss
#	apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts
#	apps/web/src/app/features/combat/combat-page/combat-page.component.ts
2026-08-20 19:42:10 +02:00
Bastian Wagner
0ce3b420e6 fix: address final-review findings (TopBar hydration, snapshot test, armor doc, inventory refresh test)
Fixes 4 Important findings from the final whole-branch review:
- /inventory never called WorldStore.load(), leaving the TopBar stuck on
  "loading" and characterLevel() silently defaulting to 1 for any character
  above level 1. Mirrors the same guard already used in HuntPageComponent.
- The combat/equipment snapshot-immutability integration test asserted only
  status/round, never the actual playerState snapshot the whole test claims
  to prove is untouched after a post-fight equip.
- Documented (comment only, no behavior change) that the demo character's
  armor dropping from the old hardcoded 6 to 0 is an intentional,
  spec-sanctioned tradeoff (Slice 0.5 spec Section19), not a bug.
- inventory.store.spec.ts's equip test used an identical inventory fixture
  before and after equip(), so a regression dropping the post-equip
  inventory re-fetch would still have passed. Now asserts the refetched
  fixture is actually reflected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 18:44:39 +02:00
Bastian Wagner
90094ba1ae fix(api): key starting-sword seed idempotency on its real unique index
The CharacterItem idempotency check was looking up by the seed's own
literal id instead of the (characterId, itemDefinitionId) unique index
that CharacterItem actually enforces. If the demo character had already
looted a worn-short-sword naturally, re-running the seed would miss
that row and try to insert a colliding duplicate, breaking seeding
instead of being a safe no-op. Look up by the real domain key and reuse
whatever id is found when wiring up the CharacterEquipment row.
2026-08-20 17:23:45 +02:00
Bastian Wagner
6178b88573 feat(api): seed the starting sword as a real, equipped CharacterItem 2026-08-20 17:11:18 +02:00
Bastian Wagner
2859a00597 test(api): prove equipping a weapon upgrade increases future combat damage 2026-08-20 17:01:45 +02:00
Bastian Wagner
820c69704f feat(api): add inventory API (GET /api/inventory) 2026-08-20 16:50:50 +02:00
Bastian Wagner
9df740e7d4 feat(api): add equipment API (GET/POST /api/equipment) 2026-08-20 16:39:23 +02:00
Bastian Wagner
3ee107694c address code review: POI order matches plan, self-documenting height reserve
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>
2026-08-20 16:33:42 +02:00
Bastian Wagner
f45f4b03cb feat(api): combat snapshots now use CharacterStatsService 2026-08-20 16:32:15 +02:00
Bastian Wagner
e5746dec5c feat(api): retire CharacterCombatStatsService; characters/me returns effective stats 2026-08-20 16:26:33 +02:00
Bastian Wagner
67c29e783f feat(api): add CharacterStatsService as authoritative effective-stat source 2026-08-20 16:20:44 +02:00
Bastian Wagner
8debb6f550 feat(api): add equipment domain errors 2026-08-20 16:16:44 +02:00
Bastian Wagner
59e88bcf9c feat(api): add character_equipment table and entity 2026-08-20 16:13:19 +02:00
Bastian Wagner
c158260623 Merge branch 'master' into worktree-local-location-view
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>
2026-08-20 16:03:59 +02:00
Bastian Wagner
60e1734f59 fix(web): keep the primary action bar on screen at every desktop width
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>
2026-08-20 15:50:31 +02:00
Bastian Wagner
6f0020137b feat(web): add the local location view at /location
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>
2026-08-20 10:51:08 +02:00
Bastian Wagner
c7e7e97252 Merge branch 'master' into worktree-local-location-view
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>
2026-08-20 10:38:18 +02:00
Bastian Wagner
a87a13fad2 Merge branch 'master' into worktree-encounter-status 2026-08-20 10:34:52 +02:00
Bastian Wagner
3c59603efb feat(hunting): show cleared encounters and resume interrupted fights
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>
2026-08-20 10:34:46 +02:00
Bastian Wagner
c85a70484f feat(world): serve local location view content from the API
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>
2026-08-20 10:30:01 +02:00
Bastian Wagner
048cf80e78 test(slice-0.4): assert migration SQL and top-bar silver/XP rendering
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>
2026-08-20 10:25:26 +02:00
Bastian Wagner
6448605a72 feat(characters): expose persisted silver on the character endpoint
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:13:04 +02:00
Bastian Wagner
40e830a321 fix(combat): lock character before combat row and harden reward-transaction test
Fix a lock-order inversion Task 8 introduced: performAction locked the
combat row first and, inside grantVictoryRewards, the character row
second -- the opposite order to startCombat (character, then combat),
creating a deadlock cycle for two concurrent requests on the same
character. performAction now locks the character first via the
existing lockCharacter helper, matching startCombat; the later re-lock
inside grantVictoryRewards is a no-op within the same transaction.

Also strengthen the test that guards the transaction contract for
grantVictoryRewards: expect.anything() would have passed even if the
data source were handed over instead of the transaction manager, since
CombatRewardService has no runtime guard against that substitution.
The test now asserts on the captured argument's identity. Verified
this is load-bearing by temporarily passing the data source in place
of the manager and confirming the test fails.

Finally, make the rollback test's unchanged-XP/silver assertions real:
the fake grantVictoryRewards now writes through the transaction
manager before throwing, so the assertions prove the rollback
discarded those writes instead of passing vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:06:50 +02:00
Bastian Wagner
2a8883d479 feat(combat): resolve victory rewards in the combat completion transaction
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:46:35 +02:00
Bastian Wagner
d9585b166a fix(rewards): resolve item definitions up front and stabilize reward item order
Resolve every rolled ItemDefinition before mutating the character or
creating the CombatReward row, so a missing definition can no longer
half-grant (XP/silver saved, reward row created, then throw). Also
make the immediate grant response and a later loadRewards replay
agree on item order by sorting both on itemDefinitionId instead of
roll/insertion order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:36:54 +02:00
Bastian Wagner
af665dc677 feat(rewards): add CombatRewardService with idempotent victory rewards
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:13:37 +02:00
Bastian Wagner
07984110bf feat(loot): add LootService with deterministic independent rolls
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:05:59 +02:00
Bastian Wagner
c58a46b7d9 feat(loot): seed tier-1 items and the ash rat and road bandit loot tables
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:59:08 +02:00
Bastian Wagner
fe130c597e feat(loot): add loot and reward migration
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:39:17 +02:00
Bastian Wagner
af9d422e8b feat(loot): add item, loot table, and combat reward entities
Declares every new TypeORM entity Slice 0.4 needs (ItemDefinition,
CharacterItem, LootTable, LootTableEntry, CombatReward, CombatRewardItem)
plus the ItemType/EquipmentSlot/ItemRarity enums, and adds the two columns
existing entities gain: Character.silver and MonsterDefinition.lootTableId.
No migration SQL or service logic yet - just schema declarations backed by
a metadata-driven schema spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:29:30 +02:00
Bastian Wagner
a5f7772a6d refactor(api): move RandomSource to shared and add rollInclusive
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:19:05 +02:00
Bastian Wagner
f33b0c0e4a feat(combat): rejoin the running combat instead of dead-ending
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.
2026-08-19 22:45:53 +02:00
Bastian Wagner
18f30a1ba1 test(combat): cover CombatService LOST persistence
CombatService only had engine-level coverage of the LOST transition.
Add service-level tests that force a loss (character.baseHp: 1) and
assert the persisted status, completedAt, further-action rejection,
and getCombat refresh behavior for a LOST combat.
2026-08-19 21:42:01 +02:00
Bastian Wagner
bb42195ef5 feat(combat): wire CombatModule into the application
Provide and export CharacterCombatStatsService from CharactersModule,
create CombatModule (registering the combat entities, controllers,
CombatService and CombatEngineService, and importing TravelModule and
CharactersModule), and register CombatModule in AppModule so the three
combat endpoints (attack, get combat, post action) are reachable from
the running app.
2026-08-19 16:19:06 +02:00
Bastian Wagner
4831dc20b0 feat(combat): add HTTP controllers for starting, reading, and acting on combats
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 16:09:08 +02:00
Bastian Wagner
498349b5eb feat(combat): add CombatService orchestration and persistence
Wires the pure combat engine, character combat stats, and domain errors
into a transactional service that validates the HuntEncounter boundary,
snapshots stats into a new Combat row, and persists engine results.
2026-08-19 16:01:17 +02:00
Bastian Wagner
90f6a8cd78 feat(combat): add combat domain errors 2026-08-19 15:50:52 +02:00
Bastian Wagner
bd8a00227f feat(characters): add temporary combat-stats stand-in for equipment 2026-08-19 15:48:11 +02:00
Bastian Wagner
edae1af39a feat(combat): add deterministic combat engine for ATTACK resolution 2026-08-19 15:43:24 +02:00
Bastian Wagner
6cb4d02613 feat(combat): add deterministic damage formula 2026-08-19 15:39:17 +02:00
Bastian Wagner
47931ff717 feat(combat): add CreateCombatSystem migration 2026-08-19 15:34:46 +02:00
Bastian Wagner
68edc04ed6 feat(combat): add combat domain enums, entities, and encounter consumption field 2026-08-19 15:27:05 +02:00
Bastian Wagner
137a18f4e7 style: fix prettier formatting drift in two API files
vertical-slice.seed.ts and danger-rating.spec.ts had line-wrapping
that no longer matched prettier's output, which a CI prettier --check
would flag. Formatting only, no logic change.
2026-08-19 14:33:41 +02:00
Bastian Wagner
bb307da0ff feat: wire HuntingController/HuntingModule and enrich current-location with monster pool
Registers HuntingModule (POST /api/hunts) into the DI graph alongside its
new entities, and adds possibleMonsters (enabled LocationMonster pool,
weight-descending, empty when hunting is disabled) to
WorldService.getCurrentLocation. Also updates the pre-existing
DB-less app.e2e-spec.ts to override HuntingModule the same way the other
feature modules already are, since it now needs a real DataSource.
2026-08-19 12:53:09 +02:00
Bastian Wagner
568478dcd2 feat: add HuntingService core domain logic for first-hunt slice
Adds startHunt's server-authoritative flow: complete due travel, verify
the location allows hunting, weighted-random-roll exactly 3 encounters
from the location's enabled monster pool inside a locked transaction
that supersedes any prior active hunt, and compute a danger rating per
encounter from the rolled monster's own stats. Mirrors TravelService's
transaction/locking pattern and error conventions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 12:24:46 +02:00
Bastian Wagner
9423c28bd8 feat: seed Aschenratte and Straßenräuber for Verbrannte Straße
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>
2026-08-19 12:08:26 +02:00
Bastian Wagner
e04827cd5a feat: add hunting system schema migration
Adds the migration and metadata spec for the hunting schema (monster
definitions, location monster spawn tables, hunts, and hunt encounters)
required by Playable Slice 0.2. Mirrors the raw-SQL style of the visible
vertical slice migration; explicitly asserts the CASCADE-vs-RESTRICT
deviation on HuntEncounter's relation to Hunt.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-19 11:55:21 +02:00
Bastian Wagner
0c2f079a6e feat: add hunt/monster domain entities and pure danger-rating helper
Adds Task 1 of Playable Slice 0.2: pure entity/enum/helper definitions
for the Hunt/Encounter system (MonsterDefinition, LocationMonster,
Hunt, HuntEncounter, EncounterType, HuntStatus, RandomSource,
DangerRating). No DB migration, module wiring, or seed data yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 11:41:09 +02:00
Bastian Wagner
d09476ba41 test: cover the successful travel start happy path in the e2e smoke suite
The DB-gated e2e suite covered health, seeded reads, and the arrivesAt
whitelist rejection, but never asserted that a valid POST /api/travel
actually transitions to TRAVELLING, that a concurrent second start is
rejected with 409/TRAVEL_ALREADY_ACTIVE, or that a travel completes and
moves the character. Added one test covering all three plus GET
/api/world/current-location reflecting the move, and restores the demo
character to its original location afterward so the suite stays safely
re-runnable.
2026-08-19 10:50:30 +02:00