AGENTS.md §33 and this branch's own constraint are English-only, and the notes were the one German document left. Content and structure are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.7 KiB
Slice 0.8.5 — Research Findings (Preliminary to the Plan)
Spec: docs/playable-slices/0.8.5-Reputation-Gated-Merchant-Offers.md
Branch/Worktree: slice/0.8.5-reputation-gated-merchant-offers in .claude/worktrees/slice-0.8.5-reputation-gated-offers
Baseline: API 410 tests / 49 suites green, Web 292 tests / 27 suites green.
What Slice 0.8 Already Delivered
Most of the mechanics already exist — 0.8.5 is largely content plus presentation, not new construction.
apps/api/src/conditions/game-condition.types.ts—GameConditionTypewithREGION_REPUTATION,WORLD_RENOWN,FLAG_SET,HAS_ITEMasSUPPORTED_CONDITION_TYPES;ComparisonOperator+compare().apps/api/src/conditions/game-condition.service.ts—evaluate()(AND logic, fail-closed) anddescribe()(per condition{condition, met}).describe()has no caller yet other than its own spec — it was built explicitly for 0.8.5.shop_offers.conditions(jsonb) exists in migration1795000000000-CreateNpcSystem.ts; the seed setsconditions: []everywhere.ShopService.getShopView()already returnsunlocked+affordable;purchase()throwsSHOP_OFFER_LOCKED(403) server-side. The frontend renders "Locked"/"Too costly" and mapsSHOP_OFFER_LOCKEDinmerchant.store.ts.
Conclusion: "Server rejects purchases when requirements are not met" (§11) is in
practice already satisfied and covered by shop.service.spec.ts. What is missing
is content, the requirement text/progress in the UI, the bag sales format, the
referral bypass and the unlock feedback.
Central Architectural Obstacle: Bags Are Not Items
shop_offers.item_definition_id is NOT NULL with an FK onto item_definitions.
The spec's two flagship offers (§4), however, are LootBagDefinition rows
(apps/api/src/loot-bags/entities/loot-bag-definition.entity.ts) — deliberately
not items (0.7.5 §6: never equipped, no loot, no combat stats). The 0.8 seed
predicts this verbatim:
"a bag is a
LootBagDefinitionrather than anItemDefinition, so selling one needs an offer shape this slice has no reason to build."
Migration required: make item_definition_id nullable, add
loot_bag_definition_id (nullable, FK → loot_bag_definitions, ON DELETE
RESTRICT), a CHECK for "exactly one of the two is set", and the unique index
IDX_shop_offers_shop_item needs a counterpart for bags. On buying a bag,
ShopService must create a CharacterLootBag row instead of stacking a
CharacterItem — idempotently, because
IDX_character_loot_bags_character_definition is unique and 0.9 §11 requires
"bag grant is idempotent".
Decisions Taken (Confirmed by the User)
- Starter bags in the seed: Remove only the Trophy Pouch from the demo seed
(
vertical-slice.seed.ts:376-392). The Hide Bag stays seeded for now, until 0.9 hands it over through the quest referral. - Hide Bag gate: Reputation gate or referral flag
(
referred-by-south-gate-warden, 0.9 §5). Needs a clearly named bypass field on the offer — expressly not a generic rules engine (§3, §7, §11 "No generalized rules engine"). Proposal:shop_offers.bypass_conditions(jsonb) with OR semantics againstconditions— the smallest extension that carries the exception case. - Third offer: Bandit Blade (already exists as an ItemDefinition,
ITEM_IDS['bandit-blade'], weaponDamage 11 / +1 Attack) behindWORLD_RENOWN >= 3.
Balancing Reference Points
- The exchange pays 2/3/5/12 reputation per trade good (
npc-content.tsEXCHANGE_RULES). - Reputation ranks: 0 Stranger, 100 Tolerated, 250 Known, 500 Recognized,
800 Trusted, 1200 Esteemed (
reputation-rank.ts). The gates are numeric, not rank-based — the spec examples say "Reputation 25". - Renown:
Character.renown, currently only one milestone (first-goods-returned, +1). Open: Renown 3 is unreachable with today's content — either a lower threshold, or the offer stays visibly locked until 0.11 (which actually matches §5, "visible rewards create goals"). - Spec example §5, verbatim: Trophy Pouch, 40 Silver, Requires Ashen Fields Reputation 25.
Files Affected
| File | Role |
|---|---|
apps/api/src/database/migrations/1796*-SellableLootBags.ts |
new: bag offers + bypass column |
apps/api/src/shops/entities/shop-offer.entity.ts |
nullable item FK, bag FK, bypassConditions |
apps/api/src/shops/shop.service.ts |
bag grant, OR bypass, requirement description via describe() |
apps/api/src/shops/shop.errors.ts |
MERCHANT_REPUTATION_TOO_LOW (§6) instead of / alongside SHOP_OFFER_LOCKED |
apps/api/src/database/seeds/npc-content.ts |
three locked offers |
apps/api/src/database/seeds/vertical-slice.seed.ts:376 |
take the Trophy Pouch out of the demo seed |
apps/web/src/app/core/api/game-api.models.ts:433 |
requirements on ShopOfferView |
apps/web/src/app/features/npc/merchant-page.component.{html,scss} |
requirement + current value per row |
apps/web/src/app/features/npc/merchant.store.ts |
unlock feedback after a trade (§9) |
Still Open Before the Plan
- The wording and carrier of the unlock feedback (§9): the store reloads the shop
after a trade anyway (
merchant.store.ts:209-212), so a before/after comparison of theunlockedflags can be drawn locally — with no new server event. That is the smallest solution and needs no endpoint. - Whether
MERCHANT_REPUTATION_TOO_LOWreplaces the existingSHOP_OFFER_LOCKEDor supplements it. Replacing it breaks the existing frontend mapping entry and one test; supplementing it (a more specific code when the violated condition is a reputation condition) satisfies §6 verbatim without a regression.