# 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` — `GameConditionType` with `REGION_REPUTATION`, `WORLD_RENOWN`, `FLAG_SET`, `HAS_ITEM` as `SUPPORTED_CONDITION_TYPES`; `ComparisonOperator` + `compare()`. - `apps/api/src/conditions/game-condition.service.ts` — `evaluate()` (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.