Files
ashen-realms/docs/superpowers/plans/2026-08-22-slice-0.8.5-research-notes.md
Bastian Wagner 481c6be5a8 docs: translate the 0.8.5 research notes to English
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>
2026-08-22 21:07:27 +02:00

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.tsGameConditionType with REGION_REPUTATION, WORLD_RENOWN, FLAG_SET, HAS_ITEM as SUPPORTED_CONDITION_TYPES; ComparisonOperator + compare().
  • apps/api/src/conditions/game-condition.service.tsevaluate() (AND logic, fail-closed) and describe() (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 migration 1795000000000-CreateNpcSystem.ts; the seed sets conditions: [] everywhere.
  • ShopService.getShopView() already returns unlocked + affordable; purchase() throws SHOP_OFFER_LOCKED (403) server-side. The frontend renders "Locked"/"Too costly" and maps SHOP_OFFER_LOCKED in merchant.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 LootBagDefinition rather than an ItemDefinition, 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)

  1. 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.
  2. 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 against conditions — the smallest extension that carries the exception case.
  3. Third offer: Bandit Blade (already exists as an ItemDefinition, ITEM_IDS['bandit-blade'], weaponDamage 11 / +1 Attack) behind WORLD_RENOWN >= 3.

Balancing Reference Points

  • The exchange pays 2/3/5/12 reputation per trade good (npc-content.ts EXCHANGE_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 the unlocked flags can be drawn locally — with no new server event. That is the smallest solution and needs no endpoint.
  • Whether MERCHANT_REPUTATION_TOO_LOW replaces the existing SHOP_OFFER_LOCKED or 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.