Ticks Slice 0.8.5's acceptance criteria against the implemented behavior, verified against the seed and service code rather than assumed, and notes that the Bandit Blade's World Renown 3 gate is deliberately unreachable until Slice 0.11 adds the milestones to reach it. Points Slice 0.9 at the concrete bypass mechanism that now exists (BORIN_OFFER_IDS.hideBag's bypassConditions flag) instead of the placeholder reference to "the quest/referral exception". Also commits the slice's plan and research-notes documents, which were untracked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.7 KiB
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—GameConditionTypemitREGION_REPUTATION,WORLD_RENOWN,FLAG_SET,HAS_ITEMalsSUPPORTED_CONDITION_TYPES;ComparisonOperator+compare().apps/api/src/conditions/game-condition.service.ts—evaluate()(UND-Logik, fail-closed) unddescribe()(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 Migration1795000000000-CreateNpcSystem.ts; Seed setzt überallconditions: [].ShopService.getShopView()liefert bereitsunlocked+affordable;purchase()wirftSHOP_OFFER_LOCKED(403) serverseitig. Frontend rendert „Locked“/„Too costly“ und mapptSHOP_OFFER_LOCKEDinmerchant.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
LootBagDefinitionrather than anItemDefinition, 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)
- 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. - 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 gegenconditions— kleinste Erweiterung, die den Ausnahmefall trägt. - Drittes Angebot: Bandit Blade (existiert bereits als ItemDefinition,
ITEM_IDS['bandit-blade'], weaponDamage 11 / +1 Attack) hinterWORLD_RENOWN >= 3.
Balancing-Anhaltspunkte
- Exchange zahlt 2/3/5/12 Ruf pro Ware (
npc-content.tsEXCHANGE_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 derunlocked-Flags lokal ziehen — ohne neues Server-Event. Das ist die kleinste Lösung und braucht keinen Endpoint. - Ob
MERCHANT_REPUTATION_TOO_LOWden bestehendenSHOP_OFFER_LOCKEDersetzt 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.