# Slice 0.8.5 — Rechercheergebnisse (Vorstufe zum 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 grün, Web 292 Tests / 27 Suites grün. ## Was Slice 0.8 bereits geliefert hat Der Großteil der Mechanik existiert schon — 0.8.5 ist überwiegend Content plus Präsentation, nicht Neubau. - `apps/api/src/conditions/game-condition.types.ts` — `GameConditionType` mit `REGION_REPUTATION`, `WORLD_RENOWN`, `FLAG_SET`, `HAS_ITEM` als `SUPPORTED_CONDITION_TYPES`; `ComparisonOperator` + `compare()`. - `apps/api/src/conditions/game-condition.service.ts` — `evaluate()` (UND-Logik, fail-closed) und `describe()` (pro Bedingung `{condition, met}`). `describe()` hat **noch keinen Aufrufer** außer seinem eigenen Spec — es wurde explizit für 0.8.5 gebaut. - `shop_offers.conditions` (jsonb) existiert in Migration `1795000000000-CreateNpcSystem.ts`; Seed setzt überall `conditions: []`. - `ShopService.getShopView()` liefert bereits `unlocked` + `affordable`; `purchase()` wirft `SHOP_OFFER_LOCKED` (403) serverseitig. Frontend rendert „Locked“/„Too costly“ und mappt `SHOP_OFFER_LOCKED` in `merchant.store.ts`. Fazit: „Server rejects purchases when requirements are not met“ (§11) ist faktisch schon erfüllt und durch `shop.service.spec.ts` getestet. Fehlend sind Content, Anforderungs-Text/Fortschritt in der UI, das Bag-Verkaufsformat, der Referral-Bypass und das Unlock-Feedback. ## Zentrale Architektur-Hürde: Taschen sind keine Items `shop_offers.item_definition_id` ist NOT NULL mit FK auf `item_definitions`. Die beiden Vorzeige-Angebote der Spec (§4) sind aber `LootBagDefinition`-Zeilen (`apps/api/src/loot-bags/entities/loot-bag-definition.entity.ts`) — bewusst keine Items (0.7.5 §6: nie ausgerüstet, kein Loot, keine Kampfwerte). Der 0.8-Seed sagt das wörtlich voraus: > „a bag is a `LootBagDefinition` rather than an `ItemDefinition`, so selling > one needs an offer shape this slice has no reason to build.“ **Nötige Migration:** `item_definition_id` nullable machen, `loot_bag_definition_id` (nullable, FK → `loot_bag_definitions`, ON DELETE RESTRICT) ergänzen, CHECK „genau eine der beiden gesetzt“, und der Unique-Index `IDX_shop_offers_shop_item` braucht ein Gegenstück für Taschen. `ShopService` muss beim Kauf einer Tasche eine `CharacterLootBag`-Zeile anlegen statt `CharacterItem` zu stapeln — idempotent, weil `IDX_character_loot_bags_character_definition` unique ist und 0.9 §11 „bag grant is idempotent“ verlangt. ## Getroffene Entscheidungen (vom Nutzer bestätigt) 1. **Starter-Bags im Seed:** Nur die Trophy Pouch aus dem Demo-Seed entfernen (`vertical-slice.seed.ts:376-392`). Die Hide Bag bleibt vorerst geseedet, bis 0.9 sie über die Quest-Referral vergibt. 2. **Hide-Bag-Gate:** Ruf-Gate **oder** Referral-Flag (`referred-by-south-gate-warden`, 0.9 §5). Braucht ein klar benanntes Bypass-Feld am Angebot — ausdrücklich *keine* generische Regel-Engine (§3, §7, §11 „No generalized rules engine“). Vorschlag: `shop_offers.bypass_conditions` (jsonb) mit ODER-Semantik gegen `conditions` — kleinste Erweiterung, die den Ausnahmefall trägt. 3. **Drittes Angebot:** Bandit Blade (existiert bereits als ItemDefinition, `ITEM_IDS['bandit-blade']`, weaponDamage 11 / +1 Attack) hinter `WORLD_RENOWN >= 3`. ## Balancing-Anhaltspunkte - Exchange zahlt 2/3/5/12 Ruf pro Ware (`npc-content.ts` `EXCHANGE_RULES`). - Reputationsränge: 0 Stranger, 100 Tolerated, 250 Known, 500 Recognized, 800 Trusted, 1200 Esteemed (`reputation-rank.ts`). Die Gates sind numerisch, nicht rangbasiert — die Spec-Beispiele nennen „Reputation 25“. - Renown: `Character.renown`, aktuell nur ein Milestone (`first-goods-returned`, +1). **Offen:** Renown 3 ist mit dem heutigen Content nicht erreichbar — entweder niedrigerer Schwellwert oder das Angebot bleibt bis 0.11 sichtbar gesperrt (was §5 „visible rewards create goals“ sogar entspricht). - Spec-Beispiel §5 wörtlich: Trophy Pouch, 40 Silber, Requires Ashen Fields Reputation 25. ## Betroffene Dateien | Datei | Rolle | |---|---| | `apps/api/src/database/migrations/1796*-SellableLootBags.ts` | neu: Bag-Angebote + Bypass-Spalte | | `apps/api/src/shops/entities/shop-offer.entity.ts` | nullable Item-FK, Bag-FK, `bypassConditions` | | `apps/api/src/shops/shop.service.ts` | Bag-Grant, ODER-Bypass, Anforderungs-Beschreibung via `describe()` | | `apps/api/src/shops/shop.errors.ts` | `MERCHANT_REPUTATION_TOO_LOW` (§6) statt/neben `SHOP_OFFER_LOCKED` | | `apps/api/src/database/seeds/npc-content.ts` | drei gesperrte Angebote | | `apps/api/src/database/seeds/vertical-slice.seed.ts:376` | Trophy Pouch aus Demo-Seed nehmen | | `apps/web/src/app/core/api/game-api.models.ts:433` | `requirements` am `ShopOfferView` | | `apps/web/src/app/features/npc/merchant-page.component.{html,scss}` | Anforderung + aktueller Wert je Zeile | | `apps/web/src/app/features/npc/merchant.store.ts` | Unlock-Feedback nach Trade (§9) | ## Noch offen vor dem Plan - Wortlaut/Träger des Unlock-Feedbacks (§9): Der Store lädt den Shop nach einem Trade ohnehin neu (`merchant.store.ts:209-212`), also lässt sich ein Vorher/Nachher-Vergleich der `unlocked`-Flags lokal ziehen — ohne neues Server-Event. Das ist die kleinste Lösung und braucht keinen Endpoint. - Ob `MERCHANT_REPUTATION_TOO_LOW` den bestehenden `SHOP_OFFER_LOCKED` ersetzt oder ergänzt. Ersetzen bricht den vorhandenen Frontend-Mapping-Eintrag und einen Test; ergänzen (spezifischer Code, wenn die verletzte Bedingung eine Reputationsbedingung ist) erfüllt §6 wörtlich ohne Regression.