Add design doc for Playable Slice 0.6.5 (Renown & Reputation Foundation)
Records every judgment call the spec left open — Renown/Reputation data model, service signatures, migration strategy, combat reward pipeline changes, and API/frontend surface — as an explicit, auditable ruling with its cost if wrong, per instruction to implement this slice without confirmation stops. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,281 @@
|
|||||||
|
# Playable Slice 0.6.5 — Renown & Reputation Foundation: Design
|
||||||
|
|
||||||
|
**Spec:** `docs/playable-slices/Ashen Realms – Playable Slice 0.6.5_ Renown & Reputation Foundation.md`
|
||||||
|
**Status:** Approved by explicit user instruction to implement without confirmation stops. Every non-obvious call the spec left open is recorded below as a ruling, with its cost if wrong, so it is auditable after the fact.
|
||||||
|
|
||||||
|
## 1. Scope confirmation
|
||||||
|
|
||||||
|
This is architectural: it removes a core progression system (Character XP/Level), adds five new tables, three new services, one enum migration, and touches the combat reward pipeline, equipment gate, and two frontend surfaces. Full brainstorming path, single spec (not decomposed — the spec is already scoped to "foundation," and splitting further would separate pieces that must land together, e.g. removing `level` and adding `renown` in the same migration).
|
||||||
|
|
||||||
|
## 2. Current-state findings that shape this design
|
||||||
|
|
||||||
|
(From a full codebase survey on `master` post-Slice-0.6-merge.)
|
||||||
|
|
||||||
|
- `Character.level`/`Character.experience` (`character.entity.ts:20-24`) are **already fully vestigial** — `character-stats.service.ts`'s `calculate()` never reads them, and `characters.service.ts` has no level-up logic anywhere. They are pure display + one validation gate. This means removing them has zero combat-math blast radius.
|
||||||
|
- The equipment level gate is the **only** consumer of `requiredLevel`: `equipment.service.ts:105-107`, `if (definition.requiredLevel > character.level) throw itemLevelRequirementNotMet();`. One call site to change.
|
||||||
|
- `danger-rating.ts` already computes danger from `{attack, armor, hp}` only, never from `level`/`experience` (`world.service.ts:214-219`, `hunting.service.ts:186-189` both pass `baseAttack`/`baseHp` directly, bypassing even `CharacterStatsService`). **Zero changes needed here** — spec §33's requirement is already satisfied by existing code.
|
||||||
|
- `CharacterItem` already supports stacking (`quantity` column, unique index on `(characterId, itemDefinitionId)`) — Trade Goods/Trophies need no new inventory infrastructure, only new `ItemType` values.
|
||||||
|
- `LootTableEntry`/`LootTable` already model exactly the "monster drops item with chance/quantity" mechanism spec §22 needs for Aschenfell/Räuberabzeichen — reuse directly, no new mechanism.
|
||||||
|
- `MonsterDefinition.experienceReward`/`silverMin`/`silverMax` are read in exactly one place: `combat-reward.service.ts:79-84`. `CombatReward.experienceGranted` persists it. No other consumers found.
|
||||||
|
- `GET /api/characters/me` (`characters.controller.ts`) is the existing character endpoint already feeding the web Topbar — the natural home for `renown`.
|
||||||
|
- One seeded item already uses `ItemType.MATERIAL` (`ash-pelt` / Aschenfell, `item-content.ts:161-168`) — **this is exactly the Trade Good the spec wants seeded**, just under the wrong `ItemType`. Migrating its type is the seed change, not a new item.
|
||||||
|
- No Grenzmarken entity/service exists anywhere — only a code comment referencing a balancing doc. Spec §23's "remove existing Grenzmarken code" is a no-op; nothing to delete.
|
||||||
|
- Migration convention: chronological timestamp-prefixed files in `apps/api/src/database/migrations/`, latest is `1790000000000-ExtendCombatEventTypes.ts`. New migration(s) start at `1791000000000`.
|
||||||
|
- Module layout convention (from `equipment/`): `X.module.ts`, `X.service.ts` (DTOs as exported interfaces at top), `X.service.spec.ts`, `X.controller.ts`, `X.errors.ts` (one `HttpException` subclass + one factory function per error code), `dto/*.dto.ts`, `entities/*.entity.ts`.
|
||||||
|
- Migration test convention: assert TypeORM entity metadata only, never execute against a live DB (matches Slice 0.6's precedent, confirmed no test suite in this project ever opens a real Postgres connection).
|
||||||
|
|
||||||
|
## 3. Rulings
|
||||||
|
|
||||||
|
Each ruling: what was decided, why, and the cost if it turns out wrong.
|
||||||
|
|
||||||
|
**R1 — Renown storage: single accumulating `renown: number` column, no separate points/rank split.**
|
||||||
|
Spec offers two options (A: points + derived rank from thresholds, B: milestones advance rank directly) and says "prefer the simplest design that fits." Renown 1–15 is itself the displayed value (unlike Reputation, which has named ranks distinct from its raw number) — so there is nothing for a second "rank" concept to add. `renownReward` on a milestone is a small positive integer (almost always 1) added directly to `character.renown`, clamped to `[1, 15]` since the V1 power-curve table only has 15 entries and the spec frames 1–15 as the whole target range for this vertical slice.
|
||||||
|
*Cost if wrong:* a later slice wanting Renown > 15 needs one clamp constant changed and the power-curve table extended — cheap, isolated.
|
||||||
|
|
||||||
|
**R2 — Renown milestone completion recomputes `baseHp`/`baseAttack` from the spec's exact power-curve table, replacing (not adding to) the previous values.**
|
||||||
|
There is no existing level-up flow to migrate (confirmed: none exists in code today). `RenownService.completeMilestone` looks up `RENOWN_BASE_STATS[newRenown]` and sets `character.baseHp`/`character.baseAttack` to that row's values directly — this is a *lookup*, not an increment, so it's idempotent and immune to double-application drift.
|
||||||
|
*Cost if wrong:* stat table is a single exported array; wrong values are a one-line data fix, not a structural problem.
|
||||||
|
|
||||||
|
**R3 — `ItemType` enum: `WEAPON`/`ARMOR` collapse into `EQUIPMENT` (the item's own `equipmentSlot` already disambiguates); `MATERIAL` becomes `TRADE_GOOD`; add `TROPHY` and `QUEST_ITEM`.**
|
||||||
|
Matches spec §16 exactly. The one existing `MATERIAL` item (Aschenfell) becomes the seeded Trade Good the spec asks for in §21 — no new item needed, just a type correction. Migration: convert the enum column to text, `UPDATE` old values to new ones, drop and recreate the Postgres enum type with only the five new values (mirrors the exact down()-migration technique already used in `1790000000000-ExtendCombatEventTypes.ts`'s rollback), convert the column back. This fully removes the old enum labels rather than leaving them as unused dead weight.
|
||||||
|
*Cost if wrong:* enum migrations are reversible via the same technique in `down()`; low risk, precedented pattern.
|
||||||
|
|
||||||
|
**R4 — `ItemDefinition.requiredLevel` column is dropped entirely, not just unused.**
|
||||||
|
Spec doesn't explicitly mandate dropping the column, only removing the validation. But keeping a dead, unused-by-any-service integer column that used to gate content is exactly the "unused player Level merely for compatibility" anti-pattern spec §13 forbids for Character — applying the same principle here for consistency. DTOs and any web-side surfacing of `requiredLevel` are removed too.
|
||||||
|
*Cost if wrong:* re-adding a dropped column is a trivial follow-up migration; no data is silently lost since dev data is disposable per spec §13.
|
||||||
|
|
||||||
|
**R5 — Single migration, not staged.**
|
||||||
|
Spec §32 allows staged migrations "if dropping old fields in the same migration would make development migration unnecessarily risky." Since all affected data is disposable dev/demo data (spec §13 explicitly sanctions this), and TypeORM's migration-per-PR convention here already treats each slice as one atomic schema step, one migration (`1791000000000-CreateRenownAndReputation.ts`) covers: Character column changes, ItemDefinition column removal, ItemType enum rebuild, and all five new tables (`reputation_factions`, `character_reputation`, `renown_milestone_definitions`, `character_renown_milestones`, `turn_in_definitions`).
|
||||||
|
*Cost if wrong:* if this ever needs re-running against real (non-disposable) data, it would need splitting — but no such data exists yet in this project.
|
||||||
|
|
||||||
|
**R6 — Character migration data step: `renown = LEAST(GREATEST(level, 1), 15)` before dropping `level`/`experience`.**
|
||||||
|
Directly implements spec §13's suggested "old Level 1 → Renown 1" mapping, clamped into the valid 1–15 range. `experience` has no analog and is simply dropped.
|
||||||
|
*Cost if wrong:* dev-only demo character; a wrong seed value is fixed by reseeding.
|
||||||
|
|
||||||
|
**R7 — `MonsterDefinition.silverMin`/`silverMax`/`experienceReward` columns and the reward-roll mechanism stay in the schema and in `CombatRewardService`; only `experienceReward` is deleted (concept removed) while `silverMin`/`silverMax` are zeroed in seed data for all currently-seeded monsters.**
|
||||||
|
Spec §15 frames a direct currency drop as a legitimate *exception* mechanism ("unless the monster explicitly has a lore-valid direct currency drop... an exception rather than the default system"), not something to delete. The cleanest way to keep the mechanism available for a future lore-valid monster while making today's content compliant is: keep the `silverMin`/`silverMax` roll in code (a monster with both set to 0 always rolls 0 — harmless), but delete `experienceReward` and the `character.experience +=` line entirely, since XP has no "legitimate exception" carve-out anywhere in the spec — it is fully abolished (§1, §33). This is the more data-driven choice per §42 (no special-case code to gate the exception; content data alone decides), and required zero seed-monster stat redesign beyond zeroing two columns.
|
||||||
|
*Cost if wrong:* if the "exception" framing turns out unwanted, deleting the roll mechanism later is a small, contained change (one method body, one DTO field).
|
||||||
|
|
||||||
|
**R8 — `CombatReward.experienceGranted` column and `CombatRewardDto.experience` field are both deleted.**
|
||||||
|
Follows directly from R7 — once XP is gone as a concept, persisting a granted-XP audit trail is dead weight. `CombatRewardDto` becomes `{ silver: number; items: CombatRewardItemDto[] }`.
|
||||||
|
*Cost if wrong:* trivial to re-add a column; no external consumer beyond this same slice's own new code.
|
||||||
|
|
||||||
|
**R9 — API surface: extend the existing `GET /api/characters/me` response with `renown: number` (replacing `level`/`experience`); add a new `GET /api/reputation` endpoint returning all enabled factions with the character's reputation (defaulting unrepresented factions to 0/Stranger); do not add a separate `GET /api/renown` endpoint.**
|
||||||
|
Spec §25 explicitly allows "a combined character endpoint... if that matches the existing architecture better" — it already does, since the Topbar already consumes this exact endpoint. A standalone `GET /api/renown` would be redundant. Reputation is faction-scoped list data with no existing analog on the character endpoint, so it gets its own endpoint, matching the reusable-component framing of spec §27.
|
||||||
|
*Cost if wrong:* adding a thin `GET /api/renown` alias later is a 5-line controller method; no architecture rework needed.
|
||||||
|
|
||||||
|
**R10 — `getCharacterReputation` returns a row for every *enabled* `ReputationFaction`, not only ones the character has actually interacted with.**
|
||||||
|
Spec §36 explicitly requires "character starts at 0 Reputation for unknown faction" to be a tested, observable behavior — this only holds if the read path itself synthesizes a default `{ reputation: 0, rank: STRANGER }` row for factions with no `CharacterReputation` record, rather than silently omitting them.
|
||||||
|
*Cost if wrong:* a caller expecting a sparse list instead of a dense one would need one filter added; no data loss.
|
||||||
|
|
||||||
|
**R11 — `RenownService`/`ReputationService` both accept an optional `EntityManager` scope parameter (mirroring `CharacterStatsService.calculate(character, scope?)` and `CombatRewardService.grantVictoryRewards(manager, combat)`), so `TurnInService` can call both inside its own transaction.**
|
||||||
|
This is the established pattern in this codebase for "service usable standalone or composed into a larger transaction." `TurnInService.turnIn` needs both a reputation grant and (for a future milestone-linked turn-in) a possible renown grant to commit atomically with the item consumption and silver grant — spec §20's atomicity requirement demands this.
|
||||||
|
*Cost if wrong:* none identified; this is the codebase's existing convention, not a new pattern.
|
||||||
|
|
||||||
|
**R12 — `TurnInDefinition` ships without `firstTurnInMilestoneKey`/`minimumQuantity`/`maximumQuantity`.**
|
||||||
|
Spec §19 explicitly lists these as "optional later fields" and says "do not add these unless actually needed" — none of §21's minimal seed content needs them.
|
||||||
|
*Cost if wrong:* additive migration later; no rework of existing rows.
|
||||||
|
|
||||||
|
**R13 — No Renown milestone content is seeded.**
|
||||||
|
Spec §5 says implement the *system* so later slices can define milestones through data, and explicitly says "do not implement all of these content milestones in Slice 0.6.5." The system is proven end-to-end via service-level tests using ad-hoc fixture milestones, not via seeded demo content. Seeding zero milestones keeps this slice's content scope minimal (matches §21's explicit minimal-content list, which does not mention milestones).
|
||||||
|
*Cost if wrong:* seeding one milestone later is a one-row `INSERT` in a seed file; the service and schema are already fully proven.
|
||||||
|
|
||||||
|
**R14 — Reputation rank thresholds and labels live in one pure resolver function (`resolveReputationRank(reputation: number)`), mirroring the existing `danger-rating.ts` pattern exactly** — a hardcoded threshold table, sorted descending, first match wins, German display labels alongside stable English keys (`STRANGER`/`Fremder`, `TOLERATED`/`Geduldet`, `KNOWN`/`Bekannt`, `RECOGNIZED`/`Anerkannt`, `TRUSTED`/`Vertraut`, `ESTEEMED`/`Geachtet`).
|
||||||
|
Spec §10 explicitly demands "one authoritative reputation-rank resolver," and the codebase already has a proven template for exactly this shape.
|
||||||
|
*Cost if wrong:* isolated pure function; trivial to adjust thresholds or labels.
|
||||||
|
|
||||||
|
**R15 — The reusable Reputation display component is built and tested but not wired into any page.**
|
||||||
|
Spec §27 explicitly says "this component does not need its final dedicated Reputation screen yet." DoD requires the presentation to exist and be reusable, not that it appear in a specific screen today (no character/merchant screen is in scope for this slice's non-goals list either).
|
||||||
|
*Cost if wrong:* wiring an already-built, already-tested component into a page is frontend integration work with no design risk.
|
||||||
|
|
||||||
|
**R16 — Web-side `CombatReward` model and victory-screen UI drop the XP block entirely** (`data-reward-experience`, the "Erfahrung"/"+X XP" line) rather than hiding it conditionally.
|
||||||
|
Spec §26/§37 explicitly forbid displaying an XP value anywhere, and R8 already removes the field from the DTO, so there is nothing to conditionally hide — the block is dead markup once the field doesn't exist.
|
||||||
|
*Cost if wrong:* re-adding a `@if` block is trivial; no architecture impact.
|
||||||
|
|
||||||
|
## 4. Data model
|
||||||
|
|
||||||
|
### Character (modified)
|
||||||
|
```
|
||||||
|
- level: integer → REMOVED
|
||||||
|
- experience: integer → REMOVED
|
||||||
|
+ renown: integer NOT NULL DEFAULT 1 (range 1–15, enforced by RenownService, not a DB CHECK constraint — matches existing style, no CHECK constraints used elsewhere in this schema)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ItemDefinition (modified)
|
||||||
|
```
|
||||||
|
- required_level: integer → REMOVED
|
||||||
|
type: item_type_enum → rebuilt: EQUIPMENT | TRADE_GOOD | TROPHY | QUEST_ITEM | CONSUMABLE
|
||||||
|
(was: WEAPON | ARMOR | MATERIAL | CONSUMABLE)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ReputationFaction (new)
|
||||||
|
```
|
||||||
|
id uuid PK
|
||||||
|
key varchar(100) UNIQUE -- e.g. 'border-guard'
|
||||||
|
name varchar(150) -- e.g. 'Grenzwacht'
|
||||||
|
description text
|
||||||
|
region_key varchar(100)
|
||||||
|
enabled boolean NOT NULL DEFAULT true
|
||||||
|
created_at, updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
### CharacterReputation (new)
|
||||||
|
```
|
||||||
|
id uuid PK
|
||||||
|
character_id uuid FK → characters ON DELETE CASCADE
|
||||||
|
faction_id uuid FK → reputation_factions ON DELETE RESTRICT
|
||||||
|
reputation integer NOT NULL DEFAULT 0
|
||||||
|
created_at, updated_at
|
||||||
|
UNIQUE(character_id, faction_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RenownMilestoneDefinition (new)
|
||||||
|
```
|
||||||
|
id uuid PK
|
||||||
|
key varchar(100) UNIQUE
|
||||||
|
name varchar(150)
|
||||||
|
description text
|
||||||
|
renown_reward integer NOT NULL
|
||||||
|
repeatable boolean NOT NULL DEFAULT false
|
||||||
|
enabled boolean NOT NULL DEFAULT true
|
||||||
|
created_at, updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
### CharacterRenownMilestone (new)
|
||||||
|
```
|
||||||
|
id uuid PK
|
||||||
|
character_id uuid FK → characters ON DELETE CASCADE
|
||||||
|
milestone_id uuid FK → renown_milestone_definitions ON DELETE RESTRICT
|
||||||
|
completed_at timestamptz NOT NULL
|
||||||
|
times_completed integer NOT NULL DEFAULT 1
|
||||||
|
UNIQUE(character_id, milestone_id) -- repeatable milestones update this same row (increment times_completed), never insert a second row
|
||||||
|
```
|
||||||
|
|
||||||
|
### TurnInDefinition (new)
|
||||||
|
```
|
||||||
|
id uuid PK
|
||||||
|
key varchar(100) UNIQUE
|
||||||
|
item_definition_id uuid FK → item_definitions ON DELETE RESTRICT
|
||||||
|
faction_id uuid FK → reputation_factions ON DELETE RESTRICT
|
||||||
|
silver_reward_per_item integer NOT NULL
|
||||||
|
reputation_reward_per_item integer NOT NULL
|
||||||
|
repeatable boolean NOT NULL DEFAULT true
|
||||||
|
enabled boolean NOT NULL DEFAULT true
|
||||||
|
created_at, updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
### CombatReward (modified)
|
||||||
|
```
|
||||||
|
- experience_granted: integer → REMOVED
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Renown power curve (verbatim from spec §4)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const RENOWN_BASE_STATS: Record<number, { baseHp: number; baseAttack: number }> = {
|
||||||
|
1: { baseHp: 100, baseAttack: 6 }, 2: { baseHp: 104, baseAttack: 6 },
|
||||||
|
3: { baseHp: 107, baseAttack: 7 }, 4: { baseHp: 111, baseAttack: 7 },
|
||||||
|
5: { baseHp: 114, baseAttack: 8 }, 6: { baseHp: 118, baseAttack: 8 },
|
||||||
|
7: { baseHp: 121, baseAttack: 9 }, 8: { baseHp: 125, baseAttack: 9 },
|
||||||
|
9: { baseHp: 128, baseAttack: 9 }, 10: { baseHp: 132, baseAttack: 10 },
|
||||||
|
11: { baseHp: 135, baseAttack: 10 }, 12: { baseHp: 139, baseAttack: 11 },
|
||||||
|
13: { baseHp: 142, baseAttack: 11 }, 14: { baseHp: 145, baseAttack: 11 },
|
||||||
|
15: { baseHp: 148, baseAttack: 12 },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Reputation ranks (verbatim from spec §10, German labels added per R14)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const REPUTATION_RANKS = [
|
||||||
|
{ threshold: 1200, key: 'ESTEEMED', label: 'Geachtet' },
|
||||||
|
{ threshold: 800, key: 'TRUSTED', label: 'Vertraut' },
|
||||||
|
{ threshold: 500, key: 'RECOGNIZED', label: 'Anerkannt' },
|
||||||
|
{ threshold: 250, key: 'KNOWN', label: 'Bekannt' },
|
||||||
|
{ threshold: 100, key: 'TOLERATED', label: 'Geduldet' },
|
||||||
|
{ threshold: 0, key: 'STRANGER', label: 'Fremder' },
|
||||||
|
] as const; // sorted descending; first threshold ≤ reputation wins
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Services
|
||||||
|
|
||||||
|
### RenownService
|
||||||
|
```ts
|
||||||
|
completeMilestone(characterId: string, milestoneKey: string, manager?: EntityManager): Promise<RenownMilestoneResult>
|
||||||
|
interface RenownMilestoneResult {
|
||||||
|
milestoneKey: string;
|
||||||
|
previousRenown: number;
|
||||||
|
newRenown: number;
|
||||||
|
renownGranted: boolean; // false if already completed (non-repeatable) or already at cap 15
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Errors: `renownMilestoneNotFound` (404), `renownMilestoneDisabled` (409), `renownMilestoneAlreadyCompleted` (409, only thrown for non-repeatable re-completion — repeatable milestones never throw this).
|
||||||
|
|
||||||
|
### ReputationService
|
||||||
|
```ts
|
||||||
|
grantReputation(characterId: string, factionKey: string, amount: number, manager?: EntityManager): Promise<ReputationGrantResult>
|
||||||
|
getCharacterReputation(characterId: string): Promise<CharacterReputationDto[]>
|
||||||
|
interface ReputationGrantResult {
|
||||||
|
factionKey: string; previousReputation: number; newReputation: number;
|
||||||
|
previousRank: string; newRank: string; rankChanged: boolean;
|
||||||
|
}
|
||||||
|
interface CharacterReputationDto {
|
||||||
|
factionKey: string; factionName: string; reputation: number; rank: string; rankLabel: string;
|
||||||
|
nextThreshold: number | null; // null once at ESTEEMED, the top rank
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Errors: `reputationFactionNotFound` (404).
|
||||||
|
|
||||||
|
### TurnInService
|
||||||
|
```ts
|
||||||
|
turnIn(characterId: string, turnInKey: string, quantity: number): Promise<TurnInResult>
|
||||||
|
interface TurnInResult {
|
||||||
|
turnInKey: string; quantityConsumed: number; silverGranted: number;
|
||||||
|
reputationResult: ReputationGrantResult;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Errors: `turnInNotFound` (404), `turnInDisabled` (409), `turnInInsufficientQuantity` (409), `turnInInvalidQuantity` (400, for quantity ≤ 0).
|
||||||
|
Transactional: locks the character, verifies/decrements `CharacterItem.quantity` (deletes the row if it reaches 0), grants silver, calls `ReputationService.grantReputation` with the same manager — all-or-nothing.
|
||||||
|
|
||||||
|
## 8. Combat reward pipeline changes
|
||||||
|
|
||||||
|
- `CombatRewardDto`: `experience` field removed.
|
||||||
|
- `grantVictoryRewards`: `experience` computation and `character.experience +=` line removed; `character.silver += silver` stays (silver roll mechanism unchanged, see R7); `CombatReward.experienceGranted` no longer set (column removed).
|
||||||
|
- Seed data: `Aschenratte.experienceReward` deleted from the seed shape (column gone from entity); `silverMin`/`silverMax` set to `0` for every currently-seeded monster (Aschenratte, Verwilderter Straßenhund, Straßenräuber, Verkohlter Plünderer, and any others in the seed file).
|
||||||
|
|
||||||
|
## 9. Equipment gate change
|
||||||
|
|
||||||
|
`equipment.service.ts`: delete the `requiredLevel > character.level` check and its error path entirely. `itemLevelRequirementNotMet` error factory and its `ITEM_LEVEL_REQUIREMENT_NOT_MET` code removed from `equipment.errors.ts` (dead code once nothing throws it).
|
||||||
|
|
||||||
|
## 10. API endpoints
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/characters/me → { ..., renown: number } (level/experience fields removed)
|
||||||
|
GET /api/reputation → CharacterReputationDto[] (dense: one entry per enabled faction)
|
||||||
|
POST /api/turn-ins → { turnInKey: string; quantity: number } → TurnInResult
|
||||||
|
```
|
||||||
|
`POST /api/turn-ins` DTO validates `turnInKey` (string) and `quantity` (positive integer) only — no reward fields accepted from the client, per spec §25's explicit prohibition list.
|
||||||
|
|
||||||
|
## 11. Frontend changes
|
||||||
|
|
||||||
|
- **`game-api.models.ts`**: `CharacterResponse.level`/`.experience` → `.renown: number`. New `ReputationEntry`/`TurnInResult` interfaces mirroring the API DTOs. `InventoryItem.item.type` (if surfaced) reflects the new `ItemType` values; `requiredLevel` removed if present anywhere in the web model.
|
||||||
|
- **Topbar** (`top-bar.component.html`): `Stufe {{ character.level }}` → `Renown {{ character.renown }}`; `data-top-bar-experience` XP block deleted entirely.
|
||||||
|
- **Combat victory screen** (`combat-page.component.html`/`.spec.ts`): `data-reward-experience` block and its three referencing tests deleted (R16); `rewards.silver` block stays.
|
||||||
|
- **New `ReputationDisplayComponent`** (`apps/web/src/app/shared/reputation-display/`): standalone component, input `entry: ReputationEntry`, renders faction name, rank label, `current / nextThreshold` (or just current if `nextThreshold` is null, at Esteemed), a progress bar. Built and unit-tested; not wired into any page (R15).
|
||||||
|
|
||||||
|
## 12. Non-goals (restated from spec §39, binding for this slice)
|
||||||
|
|
||||||
|
No merchant UI, no NPC dialogue, no quests, no bag capacity, no crafting/professions, no trading/auction house, no reputation decay, no daily caps/quests, no faction wars/competing factions, no discounts/dynamic prices, no prestige Renown, no achievements system, no Dämmerwald/Ruins reputation content, no full 15-rank unique content authoring.
|
||||||
|
|
||||||
|
## 13. Required tests (from spec §36/§37, all must exist and pass)
|
||||||
|
|
||||||
|
**Renown:** normal kill grants no Renown · milestone grants Renown · non-repeatable milestone can't double-reward · repeatable milestone can re-reward · base stats match Renown via the power-curve table · clamps at 15.
|
||||||
|
**Reputation:** starts at 0 for a faction the character has no row for · grant persists · rank resolver returns correct rank at and around every threshold · crossing a threshold reports `rankChanged: true` · not crossing reports `false` · client cannot supply reputation directly (DTO whitelist).
|
||||||
|
**Turn-In:** consumes correct quantity · grants configured silver · grants configured reputation · insufficient quantity rejects and mutates nothing · disabled/unknown turn-in rejects and mutates nothing · multi-item quantity math is server-computed, not client-supplied.
|
||||||
|
**Equipment:** an owned, sufficiently-high-tier item equips regardless of `renown` (no level gate remains) — direct regression test replacing the deleted `requiredLevel` rejection test.
|
||||||
|
**Combat reward:** WON combat grants no `experience` field at all (not zero — absent from the DTO shape) · silver stays 0 for every currently-seeded monster · loot roll (Aschenfell/Räuberabzeichen per §22) still works.
|
||||||
|
**Frontend:** Topbar shows `Renown` not `Stufe`/Level · no XP value rendered anywhere · `ReputationDisplayComponent` renders faction/rank/progress correctly at various thresholds (including the top rank with no next threshold) · turn-in request payload contains only `turnInKey`/`quantity` · combat victory screen no longer renders an XP block.
|
||||||
|
|
||||||
|
## 14. Definition of Done
|
||||||
|
|
||||||
|
Mirrors spec §44 exactly — see that section for the full checklist. Every item is covered by one or more of the rulings/sections above; nothing in §44 is left unaddressed by this design.
|
||||||
Reference in New Issue
Block a user