Commit Graph

248 Commits

Author SHA1 Message Date
Bastian Wagner
ac7750902a perf(api): load the discovery set once per location request
getCurrentLocation was calling isTravelAllowed per connection, re-querying
the character's discovered locations once per gated exit. Extract the
gating rule into a pure, synchronous WorldDiscoveryService.isRouteOpen so
the map filter can load the discovery set once and filter in memory, while
isTravelAllowed (TravelService's entry point) keeps its exact signature and
behaviour by delegating to the same rule. Also pins that discoversLocationKey
never reaches the client payload.
2026-08-23 11:28:42 +02:00
Bastian Wagner
37d025f401 feat(api): reveal the ash pit route from the watchpost hotspot 2026-08-23 11:18:20 +02:00
Bastian Wagner
aaa7146c0c feat(api): refuse travel down an undiscovered route 2026-08-23 11:05:53 +02:00
Bastian Wagner
3f336bfc34 fix(api): add activeGuard and enraged fields to CombatMonsterState 2026-08-23 10:57:28 +02:00
Bastian Wagner
604581be34 feat(api): track which locations a character has discovered 2026-08-23 10:52:45 +02:00
Bastian Wagner
420205a254 feat(api): report monster guard and enrage state in the combat DTO 2026-08-23 10:45:35 +02:00
Bastian Wagner
8a053123b9 feat(api): let wounded monsters turn aggressive 2026-08-23 10:37:22 +02:00
Bastian Wagner
739467861a feat(api): let monsters raise a breakable defensive guard 2026-08-23 10:27:01 +02:00
Bastian Wagner
dd73f708c7 feat(api): add schema for watchpost discovery and guard events 2026-08-23 10:15:10 +02:00
Bastian Wagner
c25397b544 test(seed): follow the retuned trade-good drop chances
Commit 9391645 lowered the three common guaranteed trade goods from a certain
drop to 60% and left the rare Charred Raider Insignia at 100%, but the seed
test kept asserting 1.0000 for all four and has been failing since.

Asserts the real per-item value instead of one shared constant, so the next
retune fails on the number that actually changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 09:08:31 +02:00
Bastian Wagner
2c96163a03 Merge branch 'master' into slice/0.9-first-quest-and-bag-tutorial
master turned the gate watch into painted scenery with its own portrait and
result image; Slice 0.9 turns that same hotspot into a real NPC. Kept the
NPC doorway and dropped the result text and image with it -- a hotspot
carries one or the other, never both.

The warden now uses master's `graufurt-gate-watch.png` instead of the raw
copy this branch added: same figure, already optimized. Borin keeps master's
optimized portrait for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 08:58:21 +02:00
Bastian Wagner
3a84fd5371 feat(web): show quest markers on location hotspots
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:28:55 +02:00
Bastian Wagner
ff8b7c8b78 feat(web): run quest steps from the NPC screen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:19:16 +02:00
Bastian Wagner
bc0d910190 feat(web): add the quest journal
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:12:09 +02:00
Bastian Wagner
636efb6b5a feat(web): add quest API contracts
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:05:53 +02:00
Bastian Wagner
7b738a072d art 2026-08-22 23:03:48 +02:00
Bastian Wagner
81542eb5bb feat(quests): expose the quest log and step endpoints
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:03:17 +02:00
Bastian Wagner
8f37c15654 feat(npcs): derive quest markers for the local view
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:59:22 +02:00
Bastian Wagner
b9dafd56ff feat(quests): accept, advance and complete a quest chain
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:54:44 +02:00
Bastian Wagner
d1cca086c6 feat(conditions): evaluate QUEST_ACTIVE and QUEST_COMPLETED
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:48:02 +02:00
Bastian Wagner
69a26460ae notice board 2026-08-22 22:46:58 +02:00
Bastian Wagner
e13f6533e5 feat(quests): read quest progress from owned items and bag capacity
Adds a hint to the second pelt hunt: the Hide Bag holds five of any hide,
so Tough Hides can crowd out the fifth pelt with no way forward shown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:44:53 +02:00
Bastian Wagner
fb7a96f032 layout interactions 2026-08-22 22:42:55 +02:00
Bastian Wagner
3a67dca86c feat(quests): derive the active objective from owned quantity
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:40:47 +02:00
Bastian Wagner
242c0935fa feat(quests): seed Trouble Beyond the Gate and the South Gate Warden
The demo character no longer starts with a free Basic Hide Bag: the
bagless HIDE capacity of 1 is what the quest chain exists to teach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:39:30 +02:00
Bastian Wagner
93916457cb icons 2026-08-22 22:34:51 +02:00
Bastian Wagner
f312d845af feat(quests): add quest schema, entities and migration
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:30:59 +02:00
Bastian Wagner
2dadc5c4a7 art 2026-08-22 21:56:28 +02:00
Bastian Wagner
139c5517f2 Merge branch 'slice/0.8.5-reputation-gated-merchant-offers' 2026-08-22 21:27:30 +02:00
Bastian Wagner
1a7018d790 test(seed): prove a re-seed leaves five offers at the tuned numbers
The five offer tests this branch added all read the SHOP_OFFERS constant; none
ran the seed, so the one guarantee the stable-id design exists to provide --
re-seeding does not duplicate content -- was covered by nothing. This runs
`seedVisibleVerticalSlice` twice against an in-memory shop-offer repository and
asserts the five stable ids survive.

Confirmed catchable: reverting the conflict target to
['shopId', 'itemDefinitionId'] leaves 4 rows under the fake, because both bag
offers carry `itemDefinitionId: null` and collapse into one. On Postgres it
would fail outright.

The prices (12/30/40/35/60) and thresholds (reputation 25 and 40, renown 3) are
pinned in the same test: AGENTS.md §39 forbids silent rebalancing, and the
structural tests never looked at a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:07:26 +02:00
Bastian Wagner
a454cf955e test(shop): make three nominal spec §10 cases exercise what they claim
Case 7 ("price is still required even when the reputation condition is met")
and case 3 ("sufficient regional reputation allows purchase") both built
fixtures with `conditions: []`. With no requirement present, none can be met,
so neither test touched the gate it was named after. Both now carry a
satisfied REGION_REPUTATION condition.

Case 4 ("insufficient World Renown blocks purchase") had no test at all. It
matters because a renown block must surface as SHOP_OFFER_LOCKED rather than
MERCHANT_REPUTATION_TOO_LOW -- renown is not the merchant's regard, and
telling the player to go and earn reputation would point at the wrong bar.
Verified by widening the reputation-blame check to include WORLD_RENOWN, which
fails the new test alone.

Also pins a bag offer's description to empty, so the duplicate capacity line
cannot come back through the API side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:07:25 +02:00
Bastian Wagner
f0edf89ad3 fix(shop): make purchase resolve offers the same way the view does
`purchase` listed a shop's offers with no ordering while `getShopView` orders
by `sortOrder`, so the two paths answered "which offer does this key mean" by
different rules, one of them at the database's discretion. Harmless today
because item and bag keys are disjoint, but not a difference worth keeping.

The faction lookup behind requirement labels also read every faction while the
condition engine only matches enabled ones, so a gate on a disabled faction
would have shown that faction's name next to a requirement the engine treats
as unmeetable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:07:05 +02:00
Bastian Wagner
ab8bbeae5d fix(shop): render a bag offer's capacity line once, not twice
`resolveTarget` gave a bag target the same string for `description` and
`effectSummary`, and the shop row renders both, so the slice's two flagship
offers showed "Capacity: 5 Raider Trophies" on consecutive lines. Spec §5's
worked example shows it once.

A bag definition carries no flavour text of its own, so the description is now
empty and the row omits the span entirely rather than emitting an empty one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:06:46 +02:00
Bastian Wagner
7b03310458 style: apply eslint --fix reformatting to files this slice touched
Lint was intentionally deferred through tasks 1-7 to keep each task's
diff scoped. Running it now only reformats line-wrapping in the four
shop/seed files this slice already modified; the ~86 pre-existing
problems in npcs, travel, the e2e spec and elsewhere are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:41:47 +02:00
Bastian Wagner
374926e494 feat(web): announce offers a trade just unlocked 2026-08-22 20:30:36 +02:00
Bastian Wagner
681af334bf fix(web): make ShopOfferView.effectSummary/requirements required
The server always sends both fields; an optional frontend type only
dodged a transient gap in merchant.store.spec.ts's shopView() fixture
and risked a requirement line silently failing to render if a future
fixture omitted the fields. Add the two fields to that single fixture
instead (an open offer, so null/[] are the honest values) and drop the
now-unnecessary `?? []` tolerance in the template.
2026-08-22 20:20:26 +02:00
Bastian Wagner
00784094ab feat(web): show offer requirements, current progress and effects 2026-08-22 20:15:51 +02:00
Bastian Wagner
36adc32659 feat(content): gate the trophy pouch, hide bag and bandit blade 2026-08-22 20:04:48 +02:00
Bastian Wagner
80be829309 test(shops): make the gate's guards fail when they are removed 2026-08-22 18:52:23 +02:00
Bastian Wagner
7a5e800a18 feat(shops): sell loot bags, honour bypass conditions, explain locks 2026-08-22 18:34:24 +02:00
Bastian Wagner
7e1b79315e test(shops): cover non-finite and missing-key guards in describeRequirement 2026-08-22 18:18:44 +02:00
Bastian Wagner
149a9521be feat(shops): describe offer requirements and effects in English 2026-08-22 18:14:12 +02:00
Bastian Wagner
9bd28b5e2c feat(conditions): report the current value behind each condition 2026-08-22 18:07:57 +02:00
Bastian Wagner
c295bae63a test(shops): add entity-schema cross-check for sellable loot bags
The migration spec only asserted SQL substrings against a mocked
QueryRunner and had no getMetadataArgsStorage() check that ShopOffer's
column options and partial unique indexes actually match the new
schema. Since ts-jest does not type-check in this package
(isolatedModules: true), this is the only automated guard against
entity/migration drift -- matches the house convention in
npc-system.migration.spec.ts and loot-bags.migration.spec.ts.
2026-08-22 17:59:37 +02:00
Bastian Wagner
3b28450fb1 feat(shops): let an offer sell a loot bag and carry bypass conditions
Adds a second, mutually exclusive target column (loot_bag_definition_id)
and a bypass_conditions column to shop_offers, so a later slice's quest
referral can open one offer that reputation alone would not. Keeps
shop.service.ts compiling against the now-nullable itemDefinition with
temporary non-null assertions; Task 4 replaces them with a real branch
on offer kind.
2026-08-22 17:51:47 +02:00
Bastian Wagner
8e6ec7b43c fix(web): stop hunting encounter cards from collapsing to 0px height
The host switched to display:grid + justify-items:center to center the
new flavor-text line under each card. Grid items size to their own
content by default, and .encounter-card has no in-flow content (every
child is absolutely positioned), so its intrinsic width collapsed to 0
-- taking the aspect-ratio-derived height down with it.

Revert the host to a plain block layout and center the flavor line with
its own margin instead of grid alignment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyWTLMpKYUGMz84CKrtT8
2026-08-22 17:03:11 +02:00
Bastian Wagner
081c9f83f9 docs 2026-08-22 16:41:47 +02:00
Bastian Wagner
dfa62fd152 fix: translate the last five German fixture strings found by the final review's re-review
Fixes 5 residual findings from the re-review of Playable Slice 0.6.6's
whole-branch review: two twice-hit spec files still carried invented
or seeded German fixture values (reputation-display faction name,
inventory-detail-panel item description and item name), a world store
fixture still used a German location description, and a dev comment
in reputation-content.ts referenced a faction name that no longer
exists in the code. All are literal string substitutions using
already-translated canon English text; no keys, ids, or logic changed.
2026-08-22 09:17:23 +02:00
Bastian Wagner
8418a93329 fix: correct final whole-branch review findings for 0.6.6 English content
- TRAVELLING spelling: fix TRAVELING -> TRAVELLING in travel-panel.component.html
  to match UK spelling used everywhere else (world/hunting/combat stores + API
  error messages)
- Grenzwacht -> Border Watch: fix stale German faction display name in
  reputation-display, reputation.service/controller, turn-in.service specs,
  and the vertical-slice seed spec test title
- Suedtor von Graufurt -> Graufurt South Gate: fix stale ASCII-transliterated
  German location name in hunting.service.spec.ts
- Aschenfelder(n) -> Ashen Fields: fix stale German location name/description
  in top-bar.component.spec.ts and context-panel.component.spec.ts (key
  identifiers left untouched)
- Fix 7 test titles still describing translated error messages as "German"
  across world/hunting/inventory/combat store specs
- README: update demo location names from German to their current English
  names (Graufurt South Gate, Burned Road)
- item-rarity.enum.ts: fix now-false comment claiming rarity labels are
  German; the frontend RARITY_LABELS map is English

Pure literal-string/comment substitutions; no keys, ids, or logic changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACkMEDYiwtcfchqKkiUJNX
2026-08-22 09:03:13 +02:00
Bastian Wagner
6b32f14bb5 fix(test): translate the e2e smoke test's monster name assertions to English
visible-slice.e2e-spec.ts still asserted the German monster names
'Aschenratte'/'Straßenräuber' that Task 1 already translated in the
seed data. Update the assertion to the actual English seeded values
'Ash Rat'/'Road Bandit'. This file lives outside the *.spec.ts glob
(it's matched by jest-e2e.json's *.e2e-spec.ts pattern instead), so it
was missed by prior sweeps scoped to the unit-test glob.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACkMEDYiwtcfchqKkiUJNX
2026-08-21 23:36:18 +02:00