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>
101 lines
5.7 KiB
Markdown
101 lines
5.7 KiB
Markdown
# 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.
|