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

@@ -223,14 +223,20 @@ No large modal is required.
## 11. Acceptance Criteria
- [ ] Merchant offers can define reputation/unlock requirements.
- [ ] At least two offers visibly demonstrate locked states.
- [ ] Requirements are shown to the player in English.
- [ ] Server rejects purchases when requirements are not met.
- [ ] New progression offers do not depend on level gates.
- [ ] Quest/referral unlock support exists for Slice 0.9.
- [ ] Reputation increase can visibly unlock a previously locked offer.
- [ ] No generalized rules engine was added unnecessarily.
- [x] Merchant offers can define reputation/unlock requirements.
- [x] At least two offers visibly demonstrate locked states.
- [x] Requirements are shown to the player in English.
- [x] Server rejects purchases when requirements are not met.
- [x] New progression offers do not depend on level gates.
- [x] Quest/referral unlock support exists for Slice 0.9.
- [x] Reputation increase can visibly unlock a previously locked offer.
- [x] No generalized rules engine was added unnecessarily.
> **Note (implementation):** The Bandit Blade offer is gated on World Renown 3,
> which current content cannot reach — there is one renown milestone, worth +1.
> It is deliberately left visible and locked as a long-horizon goal until
> Slice 0.11 adds the milestones that reach it (§5: visible rewards create
> goals).
---

View File

@@ -133,7 +133,16 @@ Category: HIDE
Capacity: 5
```
The grant should use the quest/referral exception supported by Slice 0.8.5.
The grant uses the referral exception built in Slice 0.8.5: the Basic Hide Bag
offer (`BORIN_OFFER_IDS.hideBag`) carries
```text
bypassConditions: [{ type: FLAG_SET, key: 'referred-by-south-gate-warden' }]
```
so setting that flag on the character's Borin state opens the offer without
changing any reputation. The offer's own gate stays at Border Watch
Reputation 40.
---

File diff suppressed because it is too large Load Diff

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.