docs: translate the 0.8.5 research notes to English
AGENTS.md §33 and this branch's own constraint are English-only, and the notes were the one German document left. Content and structure are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,100 +1,103 @@
|
|||||||
# Slice 0.8.5 — Rechercheergebnisse (Vorstufe zum Plan)
|
# Slice 0.8.5 — Research Findings (Preliminary to the Plan)
|
||||||
|
|
||||||
**Spec:** `docs/playable-slices/0.8.5-Reputation-Gated-Merchant-Offers.md`
|
**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`
|
**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.
|
**Baseline:** API 410 tests / 49 suites green, Web 292 tests / 27 suites green.
|
||||||
|
|
||||||
## Was Slice 0.8 bereits geliefert hat
|
## What Slice 0.8 Already Delivered
|
||||||
|
|
||||||
Der Großteil der Mechanik existiert schon — 0.8.5 ist überwiegend Content plus
|
Most of the mechanics already exist — 0.8.5 is largely content plus
|
||||||
Präsentation, nicht Neubau.
|
presentation, not new construction.
|
||||||
|
|
||||||
- `apps/api/src/conditions/game-condition.types.ts` — `GameConditionType` mit
|
- `apps/api/src/conditions/game-condition.types.ts` — `GameConditionType` with
|
||||||
`REGION_REPUTATION`, `WORLD_RENOWN`, `FLAG_SET`, `HAS_ITEM` als
|
`REGION_REPUTATION`, `WORLD_RENOWN`, `FLAG_SET`, `HAS_ITEM` as
|
||||||
`SUPPORTED_CONDITION_TYPES`; `ComparisonOperator` + `compare()`.
|
`SUPPORTED_CONDITION_TYPES`; `ComparisonOperator` + `compare()`.
|
||||||
- `apps/api/src/conditions/game-condition.service.ts` — `evaluate()` (UND-Logik,
|
- `apps/api/src/conditions/game-condition.service.ts` — `evaluate()` (AND logic,
|
||||||
fail-closed) und `describe()` (pro Bedingung `{condition, met}`). `describe()`
|
fail-closed) and `describe()` (per condition `{condition, met}`). `describe()`
|
||||||
hat **noch keinen Aufrufer** außer seinem eigenen Spec — es wurde explizit für
|
has **no caller yet** other than its own spec — it was built explicitly for
|
||||||
0.8.5 gebaut.
|
0.8.5.
|
||||||
- `shop_offers.conditions` (jsonb) existiert in Migration
|
- `shop_offers.conditions` (jsonb) exists in migration
|
||||||
`1795000000000-CreateNpcSystem.ts`; Seed setzt überall `conditions: []`.
|
`1795000000000-CreateNpcSystem.ts`; the seed sets `conditions: []` everywhere.
|
||||||
- `ShopService.getShopView()` liefert bereits `unlocked` + `affordable`;
|
- `ShopService.getShopView()` already returns `unlocked` + `affordable`;
|
||||||
`purchase()` wirft `SHOP_OFFER_LOCKED` (403) serverseitig. Frontend rendert
|
`purchase()` throws `SHOP_OFFER_LOCKED` (403) server-side. The frontend renders
|
||||||
„Locked“/„Too costly“ und mappt `SHOP_OFFER_LOCKED` in `merchant.store.ts`.
|
"Locked"/"Too costly" and maps `SHOP_OFFER_LOCKED` in `merchant.store.ts`.
|
||||||
|
|
||||||
Fazit: „Server rejects purchases when requirements are not met“ (§11) ist
|
Conclusion: "Server rejects purchases when requirements are not met" (§11) is in
|
||||||
faktisch schon erfüllt und durch `shop.service.spec.ts` getestet. Fehlend sind
|
practice already satisfied and covered by `shop.service.spec.ts`. What is missing
|
||||||
Content, Anforderungs-Text/Fortschritt in der UI, das Bag-Verkaufsformat, der
|
is content, the requirement text/progress in the UI, the bag sales format, the
|
||||||
Referral-Bypass und das Unlock-Feedback.
|
referral bypass and the unlock feedback.
|
||||||
|
|
||||||
## Zentrale Architektur-Hürde: Taschen sind keine Items
|
## Central Architectural Obstacle: Bags Are Not Items
|
||||||
|
|
||||||
`shop_offers.item_definition_id` ist NOT NULL mit FK auf `item_definitions`.
|
`shop_offers.item_definition_id` is NOT NULL with an FK onto `item_definitions`.
|
||||||
Die beiden Vorzeige-Angebote der Spec (§4) sind aber `LootBagDefinition`-Zeilen
|
The spec's two flagship offers (§4), however, are `LootBagDefinition` rows
|
||||||
(`apps/api/src/loot-bags/entities/loot-bag-definition.entity.ts`) — bewusst
|
(`apps/api/src/loot-bags/entities/loot-bag-definition.entity.ts`) — deliberately
|
||||||
keine Items (0.7.5 §6: nie ausgerüstet, kein Loot, keine Kampfwerte). Der
|
not items (0.7.5 §6: never equipped, no loot, no combat stats). The 0.8 seed
|
||||||
0.8-Seed sagt das wörtlich voraus:
|
predicts this verbatim:
|
||||||
|
|
||||||
> „a bag is a `LootBagDefinition` rather than an `ItemDefinition`, so selling
|
> "a bag is a `LootBagDefinition` rather than an `ItemDefinition`, so selling
|
||||||
> one needs an offer shape this slice has no reason to build.“
|
> one needs an offer shape this slice has no reason to build."
|
||||||
|
|
||||||
**Nötige Migration:** `item_definition_id` nullable machen, `loot_bag_definition_id`
|
**Migration required:** make `item_definition_id` nullable, add
|
||||||
(nullable, FK → `loot_bag_definitions`, ON DELETE RESTRICT) ergänzen, CHECK
|
`loot_bag_definition_id` (nullable, FK → `loot_bag_definitions`, ON DELETE
|
||||||
„genau eine der beiden gesetzt“, und der Unique-Index
|
RESTRICT), a CHECK for "exactly one of the two is set", and the unique index
|
||||||
`IDX_shop_offers_shop_item` braucht ein Gegenstück für Taschen. `ShopService`
|
`IDX_shop_offers_shop_item` needs a counterpart for bags. On buying a bag,
|
||||||
muss beim Kauf einer Tasche eine `CharacterLootBag`-Zeile anlegen statt
|
`ShopService` must create a `CharacterLootBag` row instead of stacking a
|
||||||
`CharacterItem` zu stapeln — idempotent, weil `IDX_character_loot_bags_character_definition`
|
`CharacterItem` — idempotently, because
|
||||||
unique ist und 0.9 §11 „bag grant is idempotent“ verlangt.
|
`IDX_character_loot_bags_character_definition` is unique and 0.9 §11 requires
|
||||||
|
"bag grant is idempotent".
|
||||||
|
|
||||||
## Getroffene Entscheidungen (vom Nutzer bestätigt)
|
## Decisions Taken (Confirmed by the User)
|
||||||
|
|
||||||
1. **Starter-Bags im Seed:** Nur die Trophy Pouch aus dem Demo-Seed entfernen
|
1. **Starter bags in the seed:** Remove only the Trophy Pouch from the demo seed
|
||||||
(`vertical-slice.seed.ts:376-392`). Die Hide Bag bleibt vorerst geseedet,
|
(`vertical-slice.seed.ts:376-392`). The Hide Bag stays seeded for now, until
|
||||||
bis 0.9 sie über die Quest-Referral vergibt.
|
0.9 hands it over through the quest referral.
|
||||||
2. **Hide-Bag-Gate:** Ruf-Gate **oder** Referral-Flag
|
2. **Hide Bag gate:** Reputation gate **or** referral flag
|
||||||
(`referred-by-south-gate-warden`, 0.9 §5). Braucht ein klar benanntes
|
(`referred-by-south-gate-warden`, 0.9 §5). Needs a clearly named bypass field
|
||||||
Bypass-Feld am Angebot — ausdrücklich *keine* generische Regel-Engine
|
on the offer — expressly *not* a generic rules engine (§3, §7, §11 "No
|
||||||
(§3, §7, §11 „No generalized rules engine“). Vorschlag:
|
generalized rules engine"). Proposal: `shop_offers.bypass_conditions` (jsonb)
|
||||||
`shop_offers.bypass_conditions` (jsonb) mit ODER-Semantik gegen
|
with OR semantics against `conditions` — the smallest extension that carries
|
||||||
`conditions` — kleinste Erweiterung, die den Ausnahmefall trägt.
|
the exception case.
|
||||||
3. **Drittes Angebot:** Bandit Blade (existiert bereits als ItemDefinition,
|
3. **Third offer:** Bandit Blade (already exists as an ItemDefinition,
|
||||||
`ITEM_IDS['bandit-blade']`, weaponDamage 11 / +1 Attack) hinter
|
`ITEM_IDS['bandit-blade']`, weaponDamage 11 / +1 Attack) behind
|
||||||
`WORLD_RENOWN >= 3`.
|
`WORLD_RENOWN >= 3`.
|
||||||
|
|
||||||
## Balancing-Anhaltspunkte
|
## Balancing Reference Points
|
||||||
|
|
||||||
- Exchange zahlt 2/3/5/12 Ruf pro Ware (`npc-content.ts` `EXCHANGE_RULES`).
|
- The exchange pays 2/3/5/12 reputation per trade good (`npc-content.ts`
|
||||||
- Reputationsränge: 0 Stranger, 100 Tolerated, 250 Known, 500 Recognized,
|
`EXCHANGE_RULES`).
|
||||||
800 Trusted, 1200 Esteemed (`reputation-rank.ts`). Die Gates sind numerisch,
|
- Reputation ranks: 0 Stranger, 100 Tolerated, 250 Known, 500 Recognized,
|
||||||
nicht rangbasiert — die Spec-Beispiele nennen „Reputation 25“.
|
800 Trusted, 1200 Esteemed (`reputation-rank.ts`). The gates are numeric, not
|
||||||
- Renown: `Character.renown`, aktuell nur ein Milestone (`first-goods-returned`,
|
rank-based — the spec examples say "Reputation 25".
|
||||||
+1). **Offen:** Renown 3 ist mit dem heutigen Content nicht erreichbar —
|
- Renown: `Character.renown`, currently only one milestone
|
||||||
entweder niedrigerer Schwellwert oder das Angebot bleibt bis 0.11 sichtbar
|
(`first-goods-returned`, +1). **Open:** Renown 3 is unreachable with today's
|
||||||
gesperrt (was §5 „visible rewards create goals“ sogar entspricht).
|
content — either a lower threshold, or the offer stays visibly locked until
|
||||||
- Spec-Beispiel §5 wörtlich: Trophy Pouch, 40 Silber, Requires Ashen Fields
|
0.11 (which actually matches §5, "visible rewards create goals").
|
||||||
|
- Spec example §5, verbatim: Trophy Pouch, 40 Silver, Requires Ashen Fields
|
||||||
Reputation 25.
|
Reputation 25.
|
||||||
|
|
||||||
## Betroffene Dateien
|
## Files Affected
|
||||||
|
|
||||||
| Datei | Rolle |
|
| File | Role |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `apps/api/src/database/migrations/1796*-SellableLootBags.ts` | neu: Bag-Angebote + Bypass-Spalte |
|
| `apps/api/src/database/migrations/1796*-SellableLootBags.ts` | new: bag offers + bypass column |
|
||||||
| `apps/api/src/shops/entities/shop-offer.entity.ts` | nullable Item-FK, Bag-FK, `bypassConditions` |
|
| `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.service.ts` | bag grant, OR bypass, requirement description via `describe()` |
|
||||||
| `apps/api/src/shops/shop.errors.ts` | `MERCHANT_REPUTATION_TOO_LOW` (§6) statt/neben `SHOP_OFFER_LOCKED` |
|
| `apps/api/src/shops/shop.errors.ts` | `MERCHANT_REPUTATION_TOO_LOW` (§6) instead of / alongside `SHOP_OFFER_LOCKED` |
|
||||||
| `apps/api/src/database/seeds/npc-content.ts` | drei gesperrte Angebote |
|
| `apps/api/src/database/seeds/npc-content.ts` | three locked offers |
|
||||||
| `apps/api/src/database/seeds/vertical-slice.seed.ts:376` | Trophy Pouch aus Demo-Seed nehmen |
|
| `apps/api/src/database/seeds/vertical-slice.seed.ts:376` | take the Trophy Pouch out of the demo seed |
|
||||||
| `apps/web/src/app/core/api/game-api.models.ts:433` | `requirements` am `ShopOfferView` |
|
| `apps/web/src/app/core/api/game-api.models.ts:433` | `requirements` on `ShopOfferView` |
|
||||||
| `apps/web/src/app/features/npc/merchant-page.component.{html,scss}` | Anforderung + aktueller Wert je Zeile |
|
| `apps/web/src/app/features/npc/merchant-page.component.{html,scss}` | requirement + current value per row |
|
||||||
| `apps/web/src/app/features/npc/merchant.store.ts` | Unlock-Feedback nach Trade (§9) |
|
| `apps/web/src/app/features/npc/merchant.store.ts` | unlock feedback after a trade (§9) |
|
||||||
|
|
||||||
## Noch offen vor dem Plan
|
## Still Open Before the Plan
|
||||||
|
|
||||||
- Wortlaut/Träger des Unlock-Feedbacks (§9): Der Store lädt den Shop nach einem
|
- The wording and carrier of the unlock feedback (§9): the store reloads the shop
|
||||||
Trade ohnehin neu (`merchant.store.ts:209-212`), also lässt sich ein
|
after a trade anyway (`merchant.store.ts:209-212`), so a before/after
|
||||||
Vorher/Nachher-Vergleich der `unlocked`-Flags lokal ziehen — ohne neues
|
comparison of the `unlocked` flags can be drawn locally — with no new server
|
||||||
Server-Event. Das ist die kleinste Lösung und braucht keinen Endpoint.
|
event. That is the smallest solution and needs no endpoint.
|
||||||
- Ob `MERCHANT_REPUTATION_TOO_LOW` den bestehenden `SHOP_OFFER_LOCKED` ersetzt
|
- Whether `MERCHANT_REPUTATION_TOO_LOW` replaces the existing
|
||||||
oder ergänzt. Ersetzen bricht den vorhandenen Frontend-Mapping-Eintrag und
|
`SHOP_OFFER_LOCKED` or supplements it. Replacing it breaks the existing
|
||||||
einen Test; ergänzen (spezifischer Code, wenn die verletzte Bedingung eine
|
frontend mapping entry and one test; supplementing it (a more specific code
|
||||||
Reputationsbedingung ist) erfüllt §6 wörtlich ohne Regression.
|
when the violated condition is a reputation condition) satisfies §6 verbatim
|
||||||
|
without a regression.
|
||||||
|
|||||||
Reference in New Issue
Block a user