docs: record 0.8.5 acceptance and the 0.9 referral mechanism

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>
This commit is contained in:
Bastian Wagner
2026-08-22 20:43:53 +02:00
parent 7b03310458
commit 81e44b2c15
4 changed files with 2576 additions and 9 deletions

View File

@@ -0,0 +1,100 @@
# 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.