diff --git a/apps/api/src/inventory/inventory.service.spec.ts b/apps/api/src/inventory/inventory.service.spec.ts index 2d39c00..3f4e4e4 100644 --- a/apps/api/src/inventory/inventory.service.spec.ts +++ b/apps/api/src/inventory/inventory.service.spec.ts @@ -19,6 +19,7 @@ function characterItem(overrides: Partial = {}): CharacterItem { itemDefinition: { key: 'worn-short-sword', name: 'Abgenutztes Kurzschwert', + description: 'Die Klinge eines Rekruten, öfter geschliffen als geführt.', rarity: ItemRarity.COMMON, type: ItemType.WEAPON, equipmentSlot: EquipmentSlot.WEAPON, @@ -64,6 +65,7 @@ describe('InventoryService', () => { item: { key: 'worn-short-sword', name: 'Abgenutztes Kurzschwert', + description: 'Die Klinge eines Rekruten, öfter geschliffen als geführt.', rarity: 'COMMON', equipmentSlot: 'WEAPON', requiredLevel: 1, diff --git a/apps/api/src/inventory/inventory.service.ts b/apps/api/src/inventory/inventory.service.ts index 3eb9a3f..a5a9a17 100644 --- a/apps/api/src/inventory/inventory.service.ts +++ b/apps/api/src/inventory/inventory.service.ts @@ -13,6 +13,7 @@ export interface InventoryItemDto { item: { key: string; name: string; + description: string; rarity: ItemRarity; equipmentSlot: EquipmentSlot | null; requiredLevel: number; @@ -56,6 +57,7 @@ export class InventoryService { item: { key: characterItem.itemDefinition.key, name: characterItem.itemDefinition.name, + description: characterItem.itemDefinition.description, rarity: characterItem.itemDefinition.rarity, equipmentSlot: characterItem.itemDefinition.equipmentSlot, requiredLevel: characterItem.itemDefinition.requiredLevel, diff --git a/apps/web/public/assets/hud-elements/panel-frame.png b/apps/web/public/assets/hud-elements/panel-frame.png new file mode 100644 index 0000000..9c9d21e Binary files /dev/null and b/apps/web/public/assets/hud-elements/panel-frame.png differ diff --git a/apps/web/public/assets/hud-elements/panel-ornament.png b/apps/web/public/assets/hud-elements/panel-ornament.png new file mode 100644 index 0000000..2ffcac6 Binary files /dev/null and b/apps/web/public/assets/hud-elements/panel-ornament.png differ diff --git a/apps/web/src/app/app.spec.ts b/apps/web/src/app/app.spec.ts index 901f009..dbc6be7 100644 --- a/apps/web/src/app/app.spec.ts +++ b/apps/web/src/app/app.spec.ts @@ -23,6 +23,7 @@ describe('App', () => { { path: 'location', children: [] }, { path: 'world', children: [] }, { path: 'hunt', children: [] }, + { path: 'inventory', children: [] }, ]), { provide: WorldStore, @@ -112,6 +113,16 @@ describe('App', () => { expect(fixture.nativeElement.querySelector('app-context-panel')).toBeNull(); }); + it('drops the shell context rail on /inventory, which needs all three of its own columns', async () => { + const fixture = TestBed.createComponent(AppShellComponent); + const router = TestBed.inject(Router); + await router.navigateByUrl('/inventory'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.nativeElement.querySelector('app-context-panel')).toBeNull(); + }); + it('keeps the shell context rail on the map', async () => { const fixture = TestBed.createComponent(AppShellComponent); const router = TestBed.inject(Router); diff --git a/apps/web/src/app/core/api/game-api.models.ts b/apps/web/src/app/core/api/game-api.models.ts index c1cc082..367dc30 100644 --- a/apps/web/src/app/core/api/game-api.models.ts +++ b/apps/web/src/app/core/api/game-api.models.ts @@ -228,6 +228,7 @@ export interface InventoryItem { item: { key: string; name: string; + description: string; rarity: ItemRarity; equipmentSlot: EquipmentSlot | null; requiredLevel: number; diff --git a/apps/web/src/app/features/inventory/inventory-detail-panel.component.html b/apps/web/src/app/features/inventory/inventory-detail-panel.component.html index e981664..d43d65b 100644 --- a/apps/web/src/app/features/inventory/inventory-detail-panel.component.html +++ b/apps/web/src/app/features/inventory/inventory-detail-panel.component.html @@ -1,60 +1,85 @@ -@if (item(); as item) { -
-
- -
-

{{ item.item.name }}

-

{{ rarityLabel() }}

- @if (slotLabel(); as slot) { -

{{ slot }} · Stufe {{ item.item.requiredLevel }}

- } -
-
+
+

Gewählter Gegenstand

- @if (statRows().length) { -
- @for (row of statRows(); track row.label) { -
-
{{ row.label }}
+ @if (item(); as item) { +
+
+ + + +
+

{{ item.item.name }}

+

{{ rarityLabel() }}

+

{{ slotLabel() ?? typeLabel() }}

+
+
+ + @if (statRows().length) { +
+ @for (row of statRows(); track row.label) { +
+
{{ row.label }}
+
+ {{ row.value }} + @if (row.diff !== null && row.diff !== 0) { + + ({{ row.diff > 0 ? '+' : '' }}{{ row.diff }}) + + } +
+
+ } +
+ } + + @if (isEquippable()) { +
+
+
Benötigte Stufe
- {{ row.value }} - @if (row.diff !== null && row.diff !== 0) { - - ({{ row.diff > 0 ? '+' : '' }}{{ row.diff }}) - - } + + {{ item.item.requiredLevel }} +
- } -
- } - -
- @if (item.equipped) { - Ausgerüstet - } @else if (!isEquippable()) { - Nicht ausrüstbar - } @else if (!meetsLevelRequirement()) { - - } @else { - +
} - -
-} @else { -

Wähle einen Gegenstand aus deinem Inventar.

-} + + @if (item.item.description) { +

{{ item.item.description }}

+ } + +
+ @if (item.equipped) { + Ausgerüstet + } @else if (!isEquippable()) { + Nicht ausrüstbar + } @else if (!meetsLevelRequirement()) { + + } @else { + + } +
+ + } @else { +

Wähle einen Gegenstand aus deinem Inventar.

+ } + diff --git a/apps/web/src/app/features/inventory/inventory-detail-panel.component.scss b/apps/web/src/app/features/inventory/inventory-detail-panel.component.scss index 49fb865..48172ff 100644 --- a/apps/web/src/app/features/inventory/inventory-detail-panel.component.scss +++ b/apps/web/src/app/features/inventory/inventory-detail-panel.component.scss @@ -2,99 +2,177 @@ display: block; } -.inventory-detail { - padding: var(--ar-space-4); - border: 1px solid var(--ar-border-highlight); - border-radius: var(--ar-radius-sm); - background: - linear-gradient(125deg, rgb(255 255 255 / 0.045), transparent 42%), rgb(12 15 17 / 0.96); - box-shadow: var(--ar-shadow-raised); +.detail { + block-size: 100%; } -.inventory-detail__header { +.detail__body { + display: grid; + gap: var(--ar-space-3); + + /* Rarity tints the name and the icon frame, nothing else — the numbers stay + the loudest thing in the panel. */ + --detail-rarity: var(--ar-text); + --detail-rarity-edge: var(--ar-border-highlight); +} + +.detail__body--rare { + --detail-rarity: var(--ar-blue); + --detail-rarity-edge: var(--ar-blue); +} + +.detail__body--epic { + --detail-rarity: var(--ar-gold); + --detail-rarity-edge: var(--ar-gold); +} + +/* ---------- identity ---------- */ + +.detail__head { display: flex; gap: var(--ar-space-3); - align-items: center; - margin-block-end: var(--ar-space-3); + align-items: flex-start; } -.inventory-detail__icon { - inline-size: 4rem; - block-size: 4rem; - padding: var(--ar-space-1); - border: 1px solid var(--ar-border); +.detail__portrait { + display: grid; + flex: 0 0 auto; + place-items: center; + inline-size: 4.25rem; + block-size: 4.25rem; + padding: 0.3rem; + border: 1px solid var(--detail-rarity-edge); background: linear-gradient(180deg, #1b1f22, #0d1012); + box-shadow: inset 0 0.15rem 0.6rem rgb(0 0 0 / 0.8); +} + +.detail__portrait img { + inline-size: 100%; + block-size: 100%; object-fit: contain; } -.inventory-detail__name { - margin: 0; - color: var(--ar-text); - font-family: Georgia, 'Times New Roman', serif; - font-size: 1.15rem; - font-weight: 400; +.detail__ident { + min-inline-size: 0; + padding-block-start: 0.15rem; } -.inventory-detail__rarity { +.detail__name { + margin: 0; + color: var(--detail-rarity); + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.1rem; + font-weight: 400; + line-height: 1.2; +} + +.detail__rarity { + margin: 0.2rem 0 0; + color: var(--detail-rarity); + font-size: 0.7rem; + letter-spacing: 0.12em; + text-transform: uppercase; + opacity: 0.85; +} + +.detail__slot { margin: 0.15rem 0 0; color: var(--ar-text-muted); font-size: var(--ar-font-sm); - letter-spacing: 0.08em; - text-transform: uppercase; } -.inventory-detail__meta { - margin: 0.25rem 0 0; +/* ---------- numbers ---------- */ + +.detail__stats, +.detail__meta { + display: grid; + gap: 0.35rem; + margin: 0; + padding-block-start: var(--ar-space-3); + border-block-start: 1px solid rgb(85 74 57 / 0.5); +} + +.detail__stat { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--ar-space-3); +} + +.detail__stat dt { color: var(--ar-text-muted); font-size: var(--ar-font-sm); } -.inventory-detail__stats { - display: grid; - gap: var(--ar-space-2); - margin: 0 0 var(--ar-space-3); - padding-block: var(--ar-space-2); - border-block: 1px solid rgb(155 122 66 / 0.45); -} - -.inventory-detail__stat { +.detail__stat dd { display: flex; - justify-content: space-between; -} - -.inventory-detail__stat dt { - color: var(--ar-text-muted); -} - -.inventory-detail__stat dd { + gap: 0.4rem; + align-items: baseline; margin: 0; + white-space: nowrap; +} + +.detail__value { + color: var(--ar-text); font-family: Georgia, 'Times New Roman', serif; + font-size: 1rem; + font-variant-numeric: tabular-nums; } -.inventory-detail__diff--positive { - color: var(--ar-success); -} - -.inventory-detail__diff--negative { +.detail__value--unmet { color: var(--ar-danger); } -.inventory-detail__actions { - display: flex; - justify-content: center; +/* The sign is carried by the text, never by colour alone (spec §52). */ +.detail__diff { + font-size: var(--ar-font-sm); + font-variant-numeric: tabular-nums; } -.inventory-detail__equipped { +.detail__diff--positive { + color: var(--ar-success); +} + +.detail__diff--negative { + color: var(--ar-danger); +} + +/* ---------- flavour ---------- */ + +.detail__flavour { + margin: 0; + padding-block-start: var(--ar-space-3); + border-block-start: 1px solid rgb(85 74 57 / 0.5); + color: var(--ar-text-muted); + font-family: Georgia, 'Times New Roman', serif; + font-size: var(--ar-font-sm); + font-style: italic; + line-height: 1.5; +} + +/* ---------- action ---------- */ + +.detail__actions { + display: grid; + justify-items: center; + padding-block-start: var(--ar-space-3); + border-block-start: 1px solid rgb(85 74 57 / 0.5); +} + +.detail__equipped { color: var(--ar-gold); font-family: Georgia, 'Times New Roman', serif; + letter-spacing: 0.08em; } -.inventory-detail__note { +.detail__note { color: var(--ar-text-muted); + font-size: var(--ar-font-sm); font-style: italic; } -.inventory-detail__equip { +/* Same steel-blue plate as the travel button: the one committing action. */ +.detail__equip { inline-size: 100%; padding: var(--ar-space-2) var(--ar-space-4); border: 1px solid var(--ar-border-highlight); @@ -103,22 +181,25 @@ background: linear-gradient(180deg, #263b4b, #17232d); font-family: Georgia, 'Times New Roman', serif; font-size: 1rem; + letter-spacing: 0.06em; } -.inventory-detail__equip:hover:not(:disabled) { +.detail__equip:hover:not(:disabled) { border-color: #d6b26b; background: linear-gradient(180deg, #315067, #1a2c3a); } -.inventory-detail__equip:disabled { +.detail__equip:disabled { border-color: var(--ar-border); color: var(--ar-text-muted); background: #1a1c1d; } -.inventory-detail__empty { - padding: var(--ar-space-4); +.detail__empty { + margin: 0; + padding-block: var(--ar-space-5); color: var(--ar-text-muted); + font-size: var(--ar-font-sm); font-style: italic; text-align: center; } diff --git a/apps/web/src/app/features/inventory/inventory-detail-panel.component.spec.ts b/apps/web/src/app/features/inventory/inventory-detail-panel.component.spec.ts index 109e9e6..34f2dce 100644 --- a/apps/web/src/app/features/inventory/inventory-detail-panel.component.spec.ts +++ b/apps/web/src/app/features/inventory/inventory-detail-panel.component.spec.ts @@ -10,6 +10,7 @@ const wornSword: InventoryItem = { item: { key: 'worn-short-sword', name: 'Abgenutztes Kurzschwert', + description: 'Beschreibung des Gegenstands.', rarity: 'COMMON', equipmentSlot: 'WEAPON', requiredLevel: 1, @@ -28,6 +29,7 @@ const banditBlade: InventoryItem = { item: { key: 'bandit-blade', name: 'Räuberklinge', + description: 'Beschreibung des Gegenstands.', rarity: 'COMMON', equipmentSlot: 'WEAPON', requiredLevel: 1, @@ -70,6 +72,19 @@ describe('InventoryDetailPanelComponent', () => { expect(element.querySelector('[data-detail-equip]')).toBeNull(); }); + it('shows the item flavour text from the server', async () => { + const fixture = await setup({ + item: { + ...banditBlade, + item: { ...banditBlade.item, description: 'Eine grob gezahnte Klinge.' }, + }, + }); + + expect((fixture.nativeElement as HTMLElement).textContent).toContain( + 'Eine grob gezahnte Klinge.', + ); + }); + it('shows the stat comparison against the equipped item in the same slot', async () => { const fixture = await setup({ item: banditBlade, equippedItemInSlot: wornSword }); const text = (fixture.nativeElement as HTMLElement).querySelector('[data-detail-stats]')?.textContent ?? ''; diff --git a/apps/web/src/app/features/inventory/inventory-detail-panel.component.ts b/apps/web/src/app/features/inventory/inventory-detail-panel.component.ts index 291f1e0..7ac718d 100644 --- a/apps/web/src/app/features/inventory/inventory-detail-panel.component.ts +++ b/apps/web/src/app/features/inventory/inventory-detail-panel.component.ts @@ -1,16 +1,7 @@ import { Component, computed, input, output } from '@angular/core'; -import type { EquipmentSlot, InventoryItem } from '../../core/api/game-api.models'; +import type { InventoryItem } from '../../core/api/game-api.models'; import { RARITY_LABELS } from '../../shared/item-card/item-card.component'; - -const SLOT_LABELS: Readonly> = { - WEAPON: 'Waffe', - HEAD: 'Kopf', - CHEST: 'Brust', - HANDS: 'Handschuhe', - LEGS: 'Beine', - FEET: 'Stiefel', - AMULET: 'Amulett', -}; +import { SLOT_LABELS } from './inventory.labels'; interface StatRow { label: string; @@ -49,6 +40,9 @@ export class InventoryDetailPanelComponent { return slot ? SLOT_LABELS[slot] : null; }); + /** Stand-in for items with no slot: the API does not expose an item type. */ + protected readonly typeLabel = computed(() => 'Gegenstand'); + protected readonly statRows = computed(() => { const item = this.item(); if (!item) { diff --git a/apps/web/src/app/features/inventory/inventory-page.component.html b/apps/web/src/app/features/inventory/inventory-page.component.html index 1d16a21..48dd0d5 100644 --- a/apps/web/src/app/features/inventory/inventory-page.component.html +++ b/apps/web/src/app/features/inventory/inventory-page.component.html @@ -1,59 +1,123 @@ -
+

Inventar

+ @if (inventoryStore.loading() && !inventoryStore.inventory()) {

Inventar wird geladen…

} @else if (inventoryStore.inventory(); as inventory) { -
- @for (entry of inventory.items; track entry.id) { - - } @empty { -

Noch keine Gegenstände gefunden.

- } -
+
+ +
+
+

Ausrüstung

-
+ + @if (inventoryStore.equipment(); as equipment) { +
+

Werte

+
+
+
Leben
+
{{ equipment.stats.maxHp }}
+
+
+
Angriff
+
{{ equipment.stats.attack }}
+
+
+
Waffenschaden
+
{{ equipment.stats.weaponDamage }}
+
+
+
Rüstung
+
{{ equipment.stats.armor }}
+
+
+
+ } +
+ + +
+

Beutel

+ +
+ @for (entry of bagCells(); track $index) { + @if (entry) { + + } @else { + + } + } +
+ +
+ {{ bagUsed() }} / {{ bagCapacity() }} Plätze belegt + @if (bagUsed() === 0) { + Noch keine Gegenstände gefunden. + } +
+
+ + - - @if (inventoryStore.equipment(); as equipment) { -
-

Ausrüstung

-
    - @for (slot of slotOrder; track slot) { -
  • - {{ slotLabels[slot] }} - - {{ equipment.slots[slot]?.item?.name ?? 'Leer' }} - -
  • - } -
- -
-
Leben
{{ equipment.stats.maxHp }}
-
Angriff
{{ equipment.stats.attack }}
-
Waffenschaden
{{ equipment.stats.weaponDamage }}
-
Rüstung
{{ equipment.stats.armor }}
-
-
- } - +
} @if (inventoryStore.error(); as error) { @@ -63,3 +127,29 @@
} + + + +
+ {{ label }} + @if (entry) { + + {{ entry.item.name }} + } @else { + + Leer + + } +
+
diff --git a/apps/web/src/app/features/inventory/inventory-page.component.scss b/apps/web/src/app/features/inventory/inventory-page.component.scss index bc83851..f6798f7 100644 --- a/apps/web/src/app/features/inventory/inventory-page.component.scss +++ b/apps/web/src/app/features/inventory/inventory-page.component.scss @@ -1,121 +1,309 @@ -// apps/web/src/app/features/inventory/inventory-page.component.scss :host { display: block; + block-size: 100%; } +/* Fits the viewport instead of growing it, so the bag scrolls inside its own + panel rather than dragging the whole screen down. Same measure the combat + stage uses for the shell chrome above and below. */ .inventory-page { - display: grid; - grid-template-columns: 1fr 20rem; - gap: var(--ar-space-5); - align-items: start; - padding: var(--ar-space-5); -} - -.inventory-page__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); - gap: var(--ar-space-3); -} - -.inventory-page__slot { - position: relative; - padding: var(--ar-space-2); - border: 1px solid transparent; - border-radius: var(--ar-radius-sm); - background: transparent; -} - -.inventory-page__slot--selected { - border-color: var(--ar-border-highlight); - background: rgb(155 122 66 / 0.12); -} - -.inventory-page__equipped-badge { - position: absolute; - inset-block-start: 0.1rem; - inset-inline-start: 50%; - translate: -50% 0; - padding: 0.05rem 0.4rem; - border: 1px solid var(--ar-border-highlight); - border-radius: var(--ar-radius-sm); - color: var(--ar-gold); - background: rgb(9 11 13 / 0.9); - font-size: 0.65rem; - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.inventory-page__empty { - grid-column: 1 / -1; - padding: var(--ar-space-4); - color: var(--ar-text-muted); - font-style: italic; -} - -.inventory-page__side { - display: grid; - gap: var(--ar-space-4); -} - -.inventory-page__equipment { - padding: var(--ar-space-4); - border: 1px solid var(--ar-border); - border-radius: var(--ar-radius-sm); - background: var(--ar-panel); -} - -.inventory-page__equipment h3 { - margin: 0 0 var(--ar-space-2); - color: var(--ar-gold); - font-family: Georgia, 'Times New Roman', serif; - font-size: 0.95rem; - font-weight: 400; - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.inventory-page__equipment-list { - display: grid; - gap: var(--ar-space-1); - margin: 0 0 var(--ar-space-3); - padding: 0; - list-style: none; -} - -.inventory-page__equipment-list li { display: flex; + flex-direction: column; + block-size: max(34rem, calc(100dvh - 12rem)); + min-block-size: 0; + overflow: hidden; +} + +.inventory-page__title { + margin: 0 0 var(--ar-space-4); + color: var(--ar-text); + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(1.5rem, 2.6vw, 2rem); + font-weight: 400; + letter-spacing: 0.04em; + text-shadow: 0 0.1rem 0.6rem rgb(0 0 0 / 0.8); +} + +/* Three rails: what you wear, what you carry, what you are looking at. */ +.inventory-page__columns { + display: grid; + flex: 1 1 auto; + grid-template-columns: minmax(18rem, 21rem) minmax(0, 1fr) minmax(16rem, 19rem); + gap: var(--ar-space-4); + align-items: stretch; + min-block-size: 0; +} + +/* The doll takes the slack; the derived values sit under it like a plate. */ +.inventory-page__rail { + display: grid; + grid-template-rows: 1fr auto; + gap: var(--ar-space-4); + min-block-size: 0; +} + +/* Panel chrome (.ar-panel) lives in styles.scss — every screen shares it. */ + +.panel--doll { + display: grid; + grid-template-rows: auto 1fr; + min-block-size: 0; +} + +/* Title, cells, then the count pinned to the bottom edge. */ +.panel--bag { + display: grid; + grid-template-rows: auto auto 1fr; + min-block-size: 0; +} + +/* ---------- paper doll ---------- */ + +.doll { + display: grid; + grid-template-columns: auto minmax(3.5rem, 1fr) auto; + grid-template-rows: minmax(0, 1fr) auto; + gap: var(--ar-space-2) var(--ar-space-3); + min-block-size: 0; +} + +.doll__column { + display: grid; + grid-row: 1; + gap: var(--ar-space-3); + align-content: center; +} + +/* The same warrior that fights on the combat screen stands here wearing the + result. Frame 0 of the six-frame attack sheet is the resting stance; the box + is kept square so that frame lands at its own proportions and never smears. */ +.doll__figure { + grid-row: 1; + grid-column: 2; + z-index: 0; + place-self: center; + aspect-ratio: 1; + /* Wider than its column: the character stands behind the sockets rather than + squeezed between them, as in the concept. */ + inline-size: 165%; + max-block-size: 100%; + pointer-events: none; + background-image: url('/images/combat/sprites/warrior-attack-sheet-384.png'); + background-repeat: no-repeat; + background-position: left center; + background-size: 600% 100%; + filter: drop-shadow(0 0.4rem 0.9rem rgb(0 0 0 / 0.75)); +} + +.doll__below { + grid-row: 2; + grid-column: 2; + justify-self: center; +} + +.socket { + position: relative; + z-index: 1; + display: grid; + gap: 0.15rem; + justify-items: center; +} + +.socket__label { + color: var(--ar-text-muted); + font-size: 0.62rem; + letter-spacing: 0.08em; + text-transform: uppercase; + white-space: nowrap; +} + +.socket__frame { + display: grid; + place-items: center; + inline-size: 3.35rem; + block-size: 3.35rem; + padding: 0.2rem; + border: 1px solid var(--ar-border); + background: linear-gradient(180deg, #1b1f22, #0d1012); + box-shadow: inset 0 0.15rem 0.5rem rgb(0 0 0 / 0.75); +} + +.socket__frame img { + inline-size: 100%; + block-size: 100%; + object-fit: contain; +} + +/* An empty socket is a recess, not a pattern — quiet enough that a filled + one is what the eye lands on. */ +.socket__frame--empty { + border-color: rgb(85 74 57 / 0.55); + background: linear-gradient(180deg, #141719, #0a0d0f); +} + +.socket__frame--filled { + border-color: var(--ar-border-highlight); +} + +.socket__frame--filled:hover { + border-color: var(--ar-gold); +} + +.socket__frame--rare { + border-color: var(--ar-blue); +} + +.socket__frame--epic { + border-color: var(--ar-gold); +} + +.socket__frame--selected { + outline: 1px solid var(--ar-blue); + outline-offset: 1px; +} + +/* ---------- values ---------- */ + +.values { + display: grid; + gap: 0.1rem; + margin: 0; +} + +.values__row { + display: flex; + align-items: baseline; justify-content: space-between; - padding-block: var(--ar-space-1); - border-block-end: 1px solid rgb(85 74 57 / 0.4); + gap: var(--ar-space-3); + padding-block: 0.3rem; +} + +.values__row + .values__row { + border-block-start: 1px solid rgb(85 74 57 / 0.32); +} + +.values dt { + display: flex; + align-items: baseline; + gap: var(--ar-space-2); + color: var(--ar-text-muted); font-size: var(--ar-font-sm); } -.inventory-page__equipment-slot-label { - color: var(--ar-text-muted); +.values__glyph { + color: var(--ar-border-highlight); + font-size: 0.8rem; } -.inventory-page__stats { - display: grid; - gap: var(--ar-space-1); - margin: 0; - padding-block-start: var(--ar-space-2); - border-block-start: 1px solid rgb(155 122 66 / 0.45); -} - -.inventory-page__stats div { - display: flex; - justify-content: space-between; -} - -.inventory-page__stats dt { - color: var(--ar-text-muted); -} - -.inventory-page__stats dd { +.values dd { margin: 0; + color: var(--ar-text); font-family: Georgia, 'Times New Roman', serif; + font-size: 1.05rem; + font-variant-numeric: tabular-nums; } +/* ---------- the bag ---------- */ + +.bag { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + gap: var(--ar-space-2); + align-content: start; + min-block-size: 0; + overflow-y: auto; + scrollbar-color: var(--ar-border) transparent; + scrollbar-width: thin; + /* Room for the scrollbar so cells never sit under it. */ + padding-inline-end: var(--ar-space-2); +} + +.cell { + position: relative; + aspect-ratio: 1; + padding: 0.42rem; + border: 1px solid var(--ar-border); + background: linear-gradient(180deg, #1b1f22, #0d1012); + box-shadow: inset 0 0.15rem 0.5rem rgb(0 0 0 / 0.7); +} + +.cell--empty { + border-color: rgb(85 74 57 / 0.45); + background: linear-gradient(180deg, #141719, #0a0d0f); +} + +.cell.inventory-page__slot:hover { + border-color: var(--ar-border-highlight); +} + +.cell--rare { + border-color: rgb(92 169 216 / 0.75); +} + +.cell--epic { + border-color: rgb(201 164 95 / 0.8); +} + +.cell--selected { + border-color: var(--ar-blue); + box-shadow: + inset 0 0.15rem 0.5rem rgb(0 0 0 / 0.7), + 0 0 0 1px var(--ar-blue), + 0 0 0.7rem rgb(92 169 216 / 0.45); +} + +.cell__icon { + inline-size: 100%; + block-size: 100%; + object-fit: contain; +} + +.cell__quantity { + position: absolute; + inset-block-end: 0.1rem; + inset-inline-end: 0.25rem; + color: var(--ar-text); + font-size: 0.72rem; + font-variant-numeric: tabular-nums; + text-shadow: 0 0 0.3rem #000, 0 0 0.2rem #000; +} + +/* Equipped pieces live on the doll; in the bag they carry a quiet gold notch. */ +.cell__equipped { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + inline-size: 0; + block-size: 0; + border-block-start: 0.55rem solid var(--ar-gold); + border-inline-end: 0.55rem solid transparent; +} + +.bag__footer { + display: flex; + flex-wrap: wrap; + gap: var(--ar-space-3); + align-items: end; + justify-content: space-between; + align-self: end; + inline-size: 100%; + margin-block-start: var(--ar-space-4); + padding-block-start: var(--ar-space-3); + border-block-start: 1px solid rgb(85 74 57 / 0.5); + color: var(--ar-text-muted); + font-size: var(--ar-font-sm); +} + +.bag__count { + font-variant-numeric: tabular-nums; +} + +.bag__hint { + font-style: italic; +} + +/* ---------- notices ---------- */ + .inventory-page__notice { padding: var(--ar-space-4); color: var(--ar-text-muted); @@ -123,11 +311,86 @@ } .inventory-page__notice--error { + display: flex; + flex-wrap: wrap; + gap: var(--ar-space-4); + align-items: center; + justify-content: space-between; + margin-block-start: var(--ar-space-4); + border: 1px solid var(--ar-danger); + background: var(--ar-panel); color: var(--ar-danger); + text-align: start; } -@media (width < 960px) { +.inventory-page__notice--error p { + margin: 0; +} + +.inventory-page__notice--error button { + padding: var(--ar-space-2) var(--ar-space-4); + border: 1px solid var(--ar-border-highlight); + border-radius: var(--ar-radius-sm); + color: var(--ar-text); + background: linear-gradient(180deg, #23282c, #14181b); + font-family: Georgia, 'Times New Roman', serif; +} + +.inventory-page__notice--error button:hover { + border-color: var(--ar-gold); + color: var(--ar-gold); +} + +/* ---------- responsive ---------- */ + +/* Once the columns start stacking the screen is taller than the viewport, so + the page goes back to scrolling as a whole and the bag stops scrolling + inside itself — two nested scrollers would fight each other. */ +@media (width < 76rem) { .inventory-page { - grid-template-columns: 1fr; + block-size: auto; + overflow: visible; + } + + .inventory-page__columns { + grid-template-columns: minmax(16rem, 18rem) minmax(0, 1fr); + align-items: start; + } + + .inventory-page__detail { + grid-column: 1 / -1; + } + + .bag { + grid-template-columns: repeat(6, minmax(0, 1fr)); + overflow-y: visible; + } + + .panel--doll, + .panel--bag { + grid-template-rows: none; + } + + .doll { + grid-template-rows: none; + } + + .doll__figure { + inline-size: 100%; + min-block-size: 12rem; + } +} + +@media (width < 52rem) { + .inventory-page { + padding: var(--ar-space-3); + } + + .inventory-page__columns { + grid-template-columns: minmax(0, 1fr); + } + + .bag { + grid-template-columns: repeat(5, minmax(0, 1fr)); } } diff --git a/apps/web/src/app/features/inventory/inventory-page.component.spec.ts b/apps/web/src/app/features/inventory/inventory-page.component.spec.ts index 4fc928f..063fad5 100644 --- a/apps/web/src/app/features/inventory/inventory-page.component.spec.ts +++ b/apps/web/src/app/features/inventory/inventory-page.component.spec.ts @@ -17,6 +17,7 @@ const inventory: InventoryResponse = { item: { key: 'worn-short-sword', name: 'Abgenutztes Kurzschwert', + description: 'Beschreibung des Gegenstands.', rarity: 'COMMON', equipmentSlot: 'WEAPON', requiredLevel: 1, @@ -34,6 +35,7 @@ const inventory: InventoryResponse = { item: { key: 'bandit-blade', name: 'Räuberklinge', + description: 'Beschreibung des Gegenstands.', rarity: 'COMMON', equipmentSlot: 'WEAPON', requiredLevel: 1, @@ -176,6 +178,7 @@ describe('InventoryPageComponent', () => { item: { key: 'iron-helm', name: 'Eiserner Helm', + description: 'Beschreibung des Gegenstands.', rarity: 'COMMON', equipmentSlot: 'HEAD', requiredLevel: 1, diff --git a/apps/web/src/app/features/inventory/inventory-page.component.ts b/apps/web/src/app/features/inventory/inventory-page.component.ts index 270c754..0af72d0 100644 --- a/apps/web/src/app/features/inventory/inventory-page.component.ts +++ b/apps/web/src/app/features/inventory/inventory-page.component.ts @@ -1,44 +1,61 @@ +import { NgTemplateOutlet } from '@angular/common'; import { Component, OnInit, computed, inject } from '@angular/core'; -import type { EquipmentSlot } from '../../core/api/game-api.models'; -import { ItemCardComponent } from '../../shared/item-card/item-card.component'; +import type { EquipmentSlot, InventoryItem } from '../../core/api/game-api.models'; import { WorldStore } from '../world/world.store'; import { InventoryDetailPanelComponent } from './inventory-detail-panel.component'; +import { SLOT_LABELS } from './inventory.labels'; import { InventoryStore } from './inventory.store'; -const SLOT_ORDER: readonly EquipmentSlot[] = [ - 'WEAPON', - 'HEAD', - 'CHEST', - 'HANDS', - 'LEGS', - 'FEET', - 'AMULET', -]; +/** + * The paper doll reads as a body: weapon hand and worn trinkets down the left, + * armour down the right, legs beneath the figure. Only the seven slots the + * game actually models appear — no decorative rings or offhand (spec §38). + */ +const DOLL_LEFT: readonly EquipmentSlot[] = ['WEAPON', 'AMULET', 'HANDS']; +const DOLL_RIGHT: readonly EquipmentSlot[] = ['HEAD', 'CHEST', 'FEET']; +const DOLL_BELOW: EquipmentSlot = 'LEGS'; -const SLOT_LABELS: Readonly> = { - WEAPON: 'Waffe', - HEAD: 'Kopf', - CHEST: 'Brust', - HANDS: 'Handschuhe', - LEGS: 'Beine', - FEET: 'Stiefel', - AMULET: 'Amulett', -}; +// Every slot, in the order the equipment list announces them. +const SLOT_ORDER: readonly EquipmentSlot[] = [...DOLL_LEFT, ...DOLL_RIGHT, DOLL_BELOW]; + +// The bag is drawn as a fixed grid so it reads as a container with room left, +// not as a list that happens to be short. Capacity is not enforced yet +// (spec §31): the empty cells are structure, not a limit. +const BAG_COLUMNS = 8; +const BAG_MIN_CELLS = 64; @Component({ selector: 'app-inventory-page', - imports: [ItemCardComponent, InventoryDetailPanelComponent], + imports: [NgTemplateOutlet, InventoryDetailPanelComponent], templateUrl: './inventory-page.component.html', styleUrl: './inventory-page.component.scss', }) export class InventoryPageComponent implements OnInit { protected readonly inventoryStore = inject(InventoryStore); private readonly worldStore = inject(WorldStore); + + protected readonly dollLeft = DOLL_LEFT; + protected readonly dollRight = DOLL_RIGHT; + protected readonly dollBelow = DOLL_BELOW; protected readonly slotOrder = SLOT_ORDER; protected readonly slotLabels = SLOT_LABELS; protected readonly characterLevel = computed(() => this.worldStore.character()?.level ?? 1); + /** + * Pads the owned items out to a full rectangle of cells. `null` is an empty + * slot; the grid never ends on a ragged row. + */ + protected readonly bagCells = computed<(InventoryItem | null)[]>(() => { + const items = this.inventoryStore.inventory()?.items ?? []; + const filled = Math.ceil(items.length / BAG_COLUMNS) * BAG_COLUMNS; + const total = Math.max(BAG_MIN_CELLS, filled); + return Array.from({ length: total }, (_, index) => items[index] ?? null); + }); + + protected readonly bagUsed = computed(() => this.inventoryStore.inventory()?.items.length ?? 0); + protected readonly bagCapacity = computed(() => this.bagCells().length); + protected readonly equippedItemInSelectedSlot = computed(() => { const selected = this.inventoryStore.selectedItem(); if (!selected?.item.equipmentSlot) { @@ -62,11 +79,11 @@ export class InventoryPageComponent implements OnInit { this.inventoryStore.selectItem(itemId); } - protected async equipSelected(characterItemId: string): Promise { - await this.inventoryStore.equip(characterItemId); - } - protected retry(): void { void this.inventoryStore.load(); } + + protected async equipSelected(characterItemId: string): Promise { + await this.inventoryStore.equip(characterItemId); + } } diff --git a/apps/web/src/app/features/inventory/inventory.labels.ts b/apps/web/src/app/features/inventory/inventory.labels.ts new file mode 100644 index 0000000..f356260 --- /dev/null +++ b/apps/web/src/app/features/inventory/inventory.labels.ts @@ -0,0 +1,12 @@ +import type { EquipmentSlot } from '../../core/api/game-api.models'; + +/** German slot names, shared by the paper doll and the item detail panel. */ +export const SLOT_LABELS: Readonly> = { + WEAPON: 'Waffe', + HEAD: 'Kopf', + CHEST: 'Brust', + HANDS: 'Handschuhe', + LEGS: 'Beine', + FEET: 'Stiefel', + AMULET: 'Amulett', +}; diff --git a/apps/web/src/app/features/inventory/inventory.store.spec.ts b/apps/web/src/app/features/inventory/inventory.store.spec.ts index 2deb123..f2752fa 100644 --- a/apps/web/src/app/features/inventory/inventory.store.spec.ts +++ b/apps/web/src/app/features/inventory/inventory.store.spec.ts @@ -16,6 +16,7 @@ const inventory: InventoryResponse = { item: { key: 'worn-short-sword', name: 'Abgenutztes Kurzschwert', + description: 'Beschreibung des Gegenstands.', rarity: 'COMMON', equipmentSlot: 'WEAPON', requiredLevel: 1, @@ -33,6 +34,7 @@ const inventory: InventoryResponse = { item: { key: 'bandit-blade', name: 'Räuberklinge', + description: 'Beschreibung des Gegenstands.', rarity: 'COMMON', equipmentSlot: 'WEAPON', requiredLevel: 1, diff --git a/apps/web/src/app/layout/app-shell/app-shell.component.ts b/apps/web/src/app/layout/app-shell/app-shell.component.ts index 726e1f5..da31819 100644 --- a/apps/web/src/app/layout/app-shell/app-shell.component.ts +++ b/apps/web/src/app/layout/app-shell/app-shell.component.ts @@ -32,5 +32,10 @@ export class AppShellComponent { // squeeze the artwork the screen is built around. private readonly atLocation = isActive('/location', this.router); - protected readonly showContextPanel = () => !this.inCombat() && !this.atLocation(); + // The inventory is itself three columns wide — doll, bag, selected item — + // and the area panel would push the bag down to a couple of cells a row. + private readonly atInventory = isActive('/inventory', this.router); + + protected readonly showContextPanel = () => + !this.inCombat() && !this.atLocation() && !this.atInventory(); } diff --git a/apps/web/src/app/layout/side-navigation/side-navigation.component.scss b/apps/web/src/app/layout/side-navigation/side-navigation.component.scss index 5c09c0a..4a16845 100644 --- a/apps/web/src/app/layout/side-navigation/side-navigation.component.scss +++ b/apps/web/src/app/layout/side-navigation/side-navigation.component.scss @@ -1,38 +1,57 @@ +/* The rail is the painted `sidepanel-left.png`: an ornate frame with six button + plates down the top and a dragon crest at the foot. The buttons below are + laid out in percentages so they land on those painted plates at any height — + the artwork stretches to the rail, and the tracks stretch with it. + Plate geometry measured from the art (see the ratios in the grid below). */ :host { display: block; - background: var(--ar-panel); + background: var(--ar-bg) url('/assets/hud-elements/sidepanel-left.png') center / 100% 100% + no-repeat; } .side-navigation { display: grid; - align-content: start; - padding-block: var(--ar-space-3); + /* A lead-in, then one track per painted plate. */ + grid-template-rows: 5.9% repeat(6, 8%); + row-gap: 1.1%; + block-size: 100%; + /* Percentage padding resolves against width — which is what the side insets + want anyway. */ + padding-inline: 18.7% 17.1%; +} + +/* Occupies the lead-in track. Without it the first button auto-places there and + the whole stack sits one plate too high. */ +.side-navigation::before { + grid-row: 1; + content: ''; } .side-navigation__item { display: flex; + grid-row: span 1; + gap: var(--ar-space-2); align-items: center; - gap: var(--ar-space-3); - inline-size: 100%; - min-block-size: 4.3rem; - padding: var(--ar-space-3) var(--ar-space-4); + justify-content: center; + min-inline-size: 0; + padding: 0 var(--ar-space-2); border: 0; - border-block-end: 1px solid color-mix(in srgb, var(--ar-border) 65%, transparent); - border-inline-start: 3px solid transparent; color: var(--ar-text-muted); + /* The plate underneath is the button's frame; nothing is drawn on top. */ background: transparent; font: inherit; text-align: start; text-decoration: none; transition: color var(--ar-motion-fast), - background var(--ar-motion-fast), - border-color var(--ar-motion-fast); + text-shadow var(--ar-motion-fast), + filter var(--ar-motion-fast); } .side-navigation__item img { - inline-size: 2.25rem; - block-size: 2.25rem; + flex: none; + inline-size: 1.9rem; + block-size: 1.9rem; object-fit: cover; border-radius: 50%; } @@ -42,8 +61,8 @@ display: grid; flex: none; place-items: center; - inline-size: 2.25rem; - block-size: 2.25rem; + inline-size: 1.9rem; + block-size: 1.9rem; border: 1px solid var(--ar-border-highlight); border-radius: 50%; color: var(--ar-gold); @@ -51,37 +70,58 @@ } .side-navigation__glyph app-location-icon { - inline-size: 1.25rem; - block-size: 1.25rem; + inline-size: 1.1rem; + block-size: 1.1rem; } +.side-navigation__item span { + overflow: hidden; + font-size: 0.95rem; + letter-spacing: 0.02em; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Active reads as the plate catching light, not as a bar bolted beside it. */ .side-navigation__item--active { - border-inline-start-color: var(--ar-blue); - color: var(--ar-text); - background: linear-gradient(90deg, rgb(92 169 216 / 0.2), transparent); - box-shadow: inset 0 0 1.25rem rgb(92 169 216 / 0.08); + color: var(--ar-gold); + filter: drop-shadow(0 0 0.5rem rgb(201 164 95 / 0.45)); + text-shadow: 0 0 0.6rem rgb(201 164 95 / 0.5); } .side-navigation__item:not(:disabled):hover, .side-navigation__item:not(:disabled):focus-visible { color: var(--ar-text); - background: rgb(255 255 255 / 0.04); } .side-navigation__item:disabled { - opacity: 0.52; + opacity: 0.42; } +/* Narrow: the tall painted rail no longer fits, so the bar goes horizontal and + drops the artwork rather than squashing it. */ @media (width < 620px) { + :host { + background: var(--ar-panel); + } + .side-navigation { - grid-template-columns: repeat(5, minmax(0, 1fr)); - padding: 0; + grid-template-rows: none; + grid-template-columns: repeat(6, minmax(0, 1fr)); + row-gap: 0; + block-size: auto; + padding-inline: 0; + } + + .side-navigation::before { + display: none; } .side-navigation__item { justify-content: center; min-block-size: 3.5rem; padding: var(--ar-space-2); + border-block-end: 1px solid color-mix(in srgb, var(--ar-border) 65%, transparent); } .side-navigation__item span { diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index 3bf3dae..ce25f68 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -83,6 +83,85 @@ img { max-inline-size: 100%; } +/* Announced to screen readers, invisible on screen. */ +.visually-hidden { + position: absolute; + inline-size: 1px; + block-size: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + border: 0; + white-space: nowrap; +} + +/* ---------- panel chrome ---------- + The forged frame every game panel sits in. The art is `panel-frame.png`, + derived from the hand-made `panel-bg.png` by `tools/asset-gen.mjs`: the + centre-edge gems are painted out there so the frame can be nine-sliced to + any panel size, and put back below as real ornaments. */ +.ar-panel { + position: relative; + padding: var(--ar-space-3) var(--ar-space-4); + /* Fallback if the art fails to load — the frame is transparent border. */ + border: 1.6rem solid transparent; + border-image: url('/assets/hud-elements/panel-frame.png') 96 fill stretch; + background-clip: padding-box; + filter: drop-shadow(0 0.4rem 1rem rgb(0 0 0 / 0.55)); +} + +/* The gem the nine-slice cannot carry, restored at the true centre of the + top and bottom edge. */ +.ar-panel::before, +.ar-panel::after { + position: absolute; + inset-inline-start: 50%; + inline-size: 2.6rem; + block-size: 2.35rem; + content: ''; + background: url('/assets/hud-elements/panel-ornament.png') center / contain no-repeat; + pointer-events: none; + translate: -50% 0; +} + +.ar-panel::before { + inset-block-start: -1.5rem; +} + +.ar-panel::after { + inset-block-end: -1.5rem; + rotate: 180deg; +} + +.ar-panel__title { + position: relative; + margin: 0 0 var(--ar-space-4); + padding-block-end: var(--ar-space-2); + color: var(--ar-text-muted); + font-family: Georgia, 'Times New Roman', serif; + font-size: 0.82rem; + font-weight: 400; + letter-spacing: 0.22em; + text-align: center; + text-transform: uppercase; +} + +.ar-panel__title::after { + position: absolute; + inset-block-end: 0; + inset-inline: 0; + block-size: 1px; + content: ''; + background: linear-gradient( + 90deg, + transparent, + rgb(155 122 66 / 0.75) 22%, + rgb(155 122 66 / 0.75) 78%, + transparent + ); +} + @media (prefers-reduced-motion: reduce) { *, *::before, diff --git a/docs/playable-slices/0.10-Abandoned-Watchpost.md b/docs/playable-slices/0.10-Abandoned-Watchpost.md new file mode 100644 index 0000000..a98ba35 --- /dev/null +++ b/docs/playable-slices/0.10-Abandoned-Watchpost.md @@ -0,0 +1,251 @@ +# Ashen Realms – Playable Slice 0.10 + +## Abandoned Watchpost + +**Status:** Implementation Specification +**Depends on:** Slice 0.9 +**Purpose:** Expand the first region beyond the tutorial road and introduce a stronger hunt location, story investigation and the first meaningful gear/reputation check. + +--- + +## 1. Goal + +The player now has enough knowledge and basic equipment to leave the Burned Road loop and push deeper into the Ashen Fields. + +The Abandoned Watchpost should feel like the first place where the world opens slightly: + +- new location artwork +- stronger encounter pool +- a surviving/remaining NPC or investigation point +- new trade goods +- stronger equipment opportunities +- route toward the Ash Pit + +The player should feel that the Burned Road prepared them for this place. + +--- + +## 2. World Connection + +Add or activate: + +```text +Burned Road ↔ Abandoned Watchpost +``` + +Initial travel target: + +```text +Duration: ~15 seconds +Ambush chance: ~10% +``` + +Values remain balancing data. + +The player is not blocked by character level. + +If progression gating is needed, prefer world discovery / quest state / actual combat danger rather than a numeric level requirement. + +--- + +## 3. Local View + +The Abandoned Watchpost should use the generic local-location screen pattern. + +Minimum interactions: + +- Hunt +- Inspect the Watchpost +- Speak with the remaining guard/NPC if present +- Travel back to Burned Road +- discover or unlock route toward Ash Pit + +The location must not be a world-map node with no local identity. + +--- + +## 4. Encounter Pool + +Recommended initial enemies: + +- Road Bandit +- Raider Scout +- Raider Veteran +- Ash Hound / Burned Hound depending on final content naming +- optional rare Raider Captain as first elite target + +Reuse existing enemy mechanics where possible. + +This slice should deepen combinations, not add several new subsystems. + +--- + +## 5. New Enemy Roles + +### Raider Scout + +- moderate damage +- simple fast attack pattern +- primarily a loot/reputation farming target + +### Raider Veteran + +- higher armor/HP +- telegraphed Heavy Strike +- can enter a defensive stance +- encourages the player to react rather than spam Attack + +### Burned Hound + +- Bleeding +- becomes more aggressive at low HP +- reinforces previously learned status behavior + +### Raider Captain – Optional Elite + +If included in this slice: + +- combines telegraphed attack + defensive behavior +- should be dangerous on first arrival +- becomes reliably beatable after several Tier-1 upgrades +- has a focused desirable equipment drop + +Do not confuse this elite with the final **Captain of the Ashen Band** area boss in Slice 0.11. + +--- + +## 6. Loot and Bags + +The location should make both current loot categories relevant. + +Examples: + +- beast enemies → HIDE goods +- raiders → RAIDER_TROPHY goods + +This gives a reason to own both: + +- Basic Hide Bag +- Basic Trophy Pouch + +If the player does not own a Trophy Pouch yet, the default capacity of 1 still applies and should naturally point them back toward the merchant. + +No direct Silver or reputation from normal kills. + +--- + +## 7. Equipment Progression + +The Watchpost should increase the chance of meaningful Tier-1 upgrades. + +Relevant items may include: + +- Bandit Blade +- Bandit Hood +- Plunderer Gloves +- Reinforced Leather Jacket +- Watchman's Leggings + +The player does not need Best-in-Slot gear to proceed. + +The intended feeling is: + +> "The enemies here are tougher, but the drops here are also the things that make me ready for the Ash Pit." + +--- + +## 8. Story / Investigation + +A simple investigation should reveal that the road attacks are organized and point toward the Ash Pit. + +This may be implemented through: + +- NPC dialogue +- inspectable environment interaction +- quest step +- persistent discovery flag + +Keep text concise. + +Example clue: + +> "The raiders weren't using the watchpost as shelter. They were using it to watch the road. Fresh tracks lead east, toward the old ash excavation." + +--- + +## 9. Unlocking the Ash Pit Route + +The route toward the Ash Pit should become visible through world progress. + +Recommended rule: + +```text +Inspect Watchpost OR complete related objective +→ discover ash-pit connection +``` + +Avoid `requiredLevel`. + +The player may be allowed to travel there even if still undergeared once the route is discovered. + +Actual danger is the soft gate. + +--- + +## 10. Merchant/Reputation Integration + +New trade goods from the Watchpost must be accepted by the existing merchant exchange system. + +The player should now have a reason to make repeated cycles: + +```text +Graufurt +→ Burned Road +→ Watchpost +→ bags fill +→ Graufurt merchant +→ Silver + reputation + Renown +→ better supplies/bags/gear +→ return +``` + +--- + +## 11. Tests + +- travel connection works both directions as configured +- Watchpost hunt uses only its own encounter pool +- Raider Veteran defensive/telegraph mechanics resolve correctly +- trade goods map to correct loot categories +- no normal enemy grants direct money/reputation +- Watchpost investigation persists +- Ash Pit route is hidden before discovery if that rule is used +- Ash Pit route becomes available after discovery + +--- + +## 12. Acceptance Criteria + +- [ ] Abandoned Watchpost exists as a full playable location. +- [ ] Travel from Burned Road works with server-authoritative timing. +- [ ] Location has a stronger, distinct encounter pool. +- [ ] At least one stronger enemy combines previously learned mechanics. +- [ ] Both HIDE and RAIDER_TROPHY carrying systems matter. +- [ ] Tier-1 equipment progression is meaningfully improved here. +- [ ] Story/investigation points toward the Ash Pit. +- [ ] Ash Pit route can be discovered without a level gate. +- [ ] Existing merchant/reputation loop continues to work. +- [ ] All player-facing content is English. + +--- + +## 13. Out of Scope + +Do not add: + +- second region +- crafting +- procedural watchpost events +- complex stealth mechanics +- large dialogue trees +- final area boss diff --git a/docs/playable-slices/0.11-Ash-Pit-and-Ashen-Band-Captain.md b/docs/playable-slices/0.11-Ash-Pit-and-Ashen-Band-Captain.md new file mode 100644 index 0000000..3d72a68 --- /dev/null +++ b/docs/playable-slices/0.11-Ash-Pit-and-Ashen-Band-Captain.md @@ -0,0 +1,261 @@ +# Ashen Realms – Playable Slice 0.11 + +## Ash Pit & Captain of the Ashen Band + +**Status:** Implementation Specification +**Depends on:** Slice 0.10 +**Purpose:** Deliver the first region climax: dangerous regular encounters, the first real area boss and guaranteed progression loot. + +--- + +## 1. Goal + +The Ash Pit is the first clear test of whether the player has understood the Ashen Fields progression loop. + +The player should arrive with: + +- some reputation progression +- useful bag capacity +- several Tier-1 equipment upgrades +- experience with Bleeding +- experience with Telegraphing +- experience with Shield Bash / interrupt +- experience with Defend + +The area culminates in the **Captain of the Ashen Band**. + +--- + +## 2. World Connection + +Activate/discover: + +```text +Abandoned Watchpost ↔ Ash Pit +``` + +Suggested travel duration: + +```text +15–20 seconds +``` + +Travel risk can be higher than the Burned Road connection. + +The route should remain a soft gate: the player can enter once discovered, even if the enemies are still too dangerous. + +--- + +## 3. Ash Pit Local View + +The Ash Pit should immediately feel more dangerous. + +Minimum interactions: + +- Hunt +- Enter / challenge the band captain when available +- Inspect the excavation / camp +- Travel back to Abandoned Watchpost + +Artwork and UI should communicate that this is the region's current endpoint. + +--- + +## 4. Regular Encounter Pool + +Recommended enemies: + +- Ash Burrower +- Raider Veteran +- Burned Hound +- optional rare/strong raider variant + +The regular pool should be harder than the Watchpost without simply multiplying HP. + +--- + +## 5. Enemy Mechanics + +### Ash Burrower + +- durable melee enemy +- higher armor than early beasts +- teaches that some enemies are naturally slower to kill + +### Raider Veteran + +- reused known mechanics +- Heavy Strike telegraph +- defensive stance + +### Burned Hound + +- Bleeding +- increased aggression at low HP + +The Ash Pit should feel like a consolidation of the region's mechanics before the boss. + +--- + +## 6. Boss – Captain of the Ashen Band + +English display name: + +**Captain of the Ashen Band** + +The boss should combine known mechanics instead of introducing a new combat resource. + +### Phase / behavior concept + +#### Normal state + +- standard melee attack +- telegraphed Heavy Strike + +#### Defensive state + +At a configured trigger: + +- increases armor for a short duration +- player can choose to Defend/heal/manage the round rather than waste damage + +#### Low-health aggression + +Below approximately 30% HP: + +- increased pressure +- faster/more frequent dangerous action +- no unfair hidden one-shot + +--- + +## 7. Boss Reward Philosophy + +The boss must never feel unrewarding. + +On victory: + +### Guaranteed + +- one relevant high-quality Tier-1 equipment item from a configured pool +- boss trade good / trophy +- boss defeated world-state flag + +### Additional independent roll + +- chance for Ashen Blade +- chance for Charred Captain's Pendant / prestige item if already part of content + +Normal repeat kills should not directly print large amounts of Silver or reputation. + +The boss trophy can be exchanged through the merchant system for meaningful value. + +--- + +## 8. World Renown Milestone + +A first-time area-boss victory is a valid **major achievement** and may grant a one-time World Renown reward directly. + +Rules: + +- first victory only +- persisted milestone flag +- repeat kills do not repeat the milestone Renown reward + +This keeps the distinction: + +- normal kills → goods +- trade-in → routine Silver/reputation/Renown progression +- major achievements → optional one-time direct World Renown + +--- + +## 9. Boss Availability + +The boss challenge should be visible as a location action once the relevant Ash Pit discovery/story condition is met. + +Avoid a hard level requirement. + +If the player attacks too early, the boss should simply be very dangerous. + +A danger label should clearly communicate this. + +--- + +## 10. Defeat + +A boss defeat follows the general Ashen Realms defeat philosophy: + +- no item loss +- no reputation loss +- no permanent progression loss +- return to a safe location according to the existing defeat flow + +The player should be encouraged to upgrade and return. + +--- + +## 11. Region Completion State + +First boss victory should set a persisted region-progress milestone such as: + +```text +ashen-fields-boss-defeated +``` + +This can later unlock/discover the route toward the next region. + +Slice 0.12 validates and polishes the entire region; it does not need to fully implement the next region. + +--- + +## 12. Tests + +### Boss combat + +- Heavy Strike telegraphs correctly +- interrupt works where intended +- defensive phase modifies armor for correct duration +- low-health behavior triggers once/according to rules +- boss victory persists + +### Rewards + +- guaranteed Tier-1 item always grants +- special drop is independent roll +- trophy grants once per victory as configured +- first victory World Renown milestone is granted once +- repeat boss victory does not duplicate first-clear milestone reward + +### Regression + +- regular Ash Pit enemies still grant no direct XP/Silver/reputation + +--- + +## 13. Acceptance Criteria + +- [ ] Ash Pit exists as the region's hardest current location. +- [ ] Regular encounter pool combines learned mechanics. +- [ ] Captain of the Ashen Band is a dedicated boss encounter. +- [ ] Boss has telegraph, defensive behavior and low-HP pressure. +- [ ] Boss is beatable with appropriate Tier-1 progression. +- [ ] Boss always grants one relevant Tier-1 equipment item. +- [ ] Special boss loot uses a separate roll. +- [ ] First clear can grant one-time World Renown. +- [ ] Repeat clears cannot duplicate the first-clear milestone reward. +- [ ] Region completion state is persisted. +- [ ] All player-facing text is English. + +--- + +## 14. Out of Scope + +Do not implement: + +- Dusk/Gloam forest region content +- raids +- group combat +- boss matchmaking +- weekly lockouts +- complex multi-phase cinematic boss logic diff --git a/docs/playable-slices/0.12-Ashen-Fields-Complete-and-Polish.md b/docs/playable-slices/0.12-Ashen-Fields-Complete-and-Polish.md new file mode 100644 index 0000000..ecd78a6 --- /dev/null +++ b/docs/playable-slices/0.12-Ashen-Fields-Complete-and-Polish.md @@ -0,0 +1,384 @@ +# Ashen Realms – Playable Slice 0.12 + +## Ashen Fields Complete – Integration, Balancing & Polish + +**Status:** Implementation Specification +**Depends on:** Slice 0.11 +**Purpose:** Turn all systems built through 0.11 into one coherent, testable first-region experience before expanding to the next region. + +--- + +## 1. Goal + +Slice 0.12 is not primarily a new feature slice. + +It is the point where the Ashen Fields must work as **one game**. + +A fresh character should be able to play through the complete first-region loop without developer intervention: + +**Graufurt → South Gate → Burned Road → hunt → bag limit → merchant → reputation → upgrades → Abandoned Watchpost → Ash Pit → Captain of the Ashen Band.** + +If this loop is not fun and understandable, do not move on to the next region yet. + +--- + +## 2. Full Region Scope + +The complete first-region content should include at least: + +### Safe / transition locations + +- Graufurt +- Graufurt South Gate + +### Progression locations + +- Burned Road +- Abandoned Watchpost +- Ash Pit + +### Core systems + +- travel +- local location view +- hunt +- encounter choice +- turn-based combat +- Bleeding +- Telegraphing +- interrupt +- Defend +- potions/combat bag as already implemented +- trade goods +- monster categories +- loot categories +- loot bags +- merchant trade-in +- Silver +- regional reputation +- World Renown +- reputation-gated offers +- first quest chain +- Tier-1 equipment +- first area boss + +--- + +## 3. Remove Legacy Progression Assumptions + +Perform a complete audit for obsolete XP-era behavior. + +For the implemented first region: + +- normal monsters must not grant XP +- normal monsters must not grant Silver directly +- normal monsters must not grant reputation directly +- old Grenzmarken / border-token progression must not remain active unless explicitly reintroduced as a separate intentional system +- merchant offers must not use old character-level gates as the primary progression requirement + +Search: + +- seed data +- entities +- DTOs +- API responses +- combat victory code +- loot service +- UI reward summaries +- tests +- docs used by implementation agents + +Do not leave old behavior silently active beside the new reputation model. + +--- + +## 4. Tier-1 Item Completion + +Ensure the first region has a coherent fixed-item progression. + +Recommended Tier-1 set of named items: + +- Worn Shortsword +- Bandit Blade +- Ashen Blade +- Bandit Hood +- Reinforced Leather Jacket +- Plunderer Gloves +- Watchman's Leggings +- Ashen Boots +- Mark of the Border Watch +- Charred Captain's Pendant + +Not every item needs to be mandatory. + +Each relevant item must have at least one clear acquisition path: + +- monster drop +- boss guaranteed pool +- reputation-gated merchant offer +- quest reward, only where appropriate + +Avoid items that exist in the database but cannot realistically be obtained. + +--- + +## 5. Target Combat Durations + +Use the existing balancing targets: + +### Normal enemies + +**4–8 rounds** in appropriate content. + +### Elite enemies + +**6–10 rounds.** + +### Boss + +**8–14 rounds.** + +Run deterministic simulations/tests where possible and manually play representative loadouts. + +Do not balance only around a fully optimized character. + +--- + +## 6. Progression States to Test + +At minimum test these player states: + +### Fresh character + +- starter weapon +- starter armor +- no loot bags +- no reputation + +Expected: + +- Ash Rat comfortable +- Road Bandit risky but understandable +- Watchpost clearly too early + +### Early Burned Road progression + +- Basic Hide Bag +- first equipment upgrade +- some merchant reputation + +Expected: + +- Burned Road reliable +- Watchpost possible but harder + +### Mid Ashen Fields + +- several Tier-1 upgrades +- Trophy Pouch or equivalent capacity improvement +- improved reputation + +Expected: + +- Watchpost reliable +- Ash Pit dangerous but approachable + +### Region-ready + +- strong but not necessarily Best-in-Slot Tier-1 loadout + +Expected: + +- Captain of the Ashen Band reasonably beatable with correct decisions + +--- + +## 7. Bag / Return-Trip Balancing + +The bag system must create decisions without becoming tedious. + +Validate: + +- default capacity 1 is useful for the tutorial moment +- Basic Hide Bag capacity 5 feels meaningful +- trade-in trips are frequent enough to create rhythm +- trips are not so frequent that the player spends more time returning than fighting +- higher-value raider trophies do not require excessive backtracking + +Adjust bag capacity, travel duration or reward value rather than adding arbitrary shortcuts immediately. + +--- + +## 8. Merchant Economy Balancing + +Validate: + +- a first meaningful purchase is reachable in a reasonable number of hunt cycles +- reputation unlocks occur often enough to be noticed +- locked items create goals rather than frustration +- Silver does not accumulate with no useful sinks +- trade goods have visibly different value +- rare encounters feel economically exciting + +Do not target a perfect final economy. Target a coherent first-region economy. + +--- + +## 9. World Renown Balancing + +World Renown is the long-term level-like progression. + +For this slice: + +- it must visibly increase during normal region play +- it must not race upward from every individual kill +- merchant exchange and major milestones are the key sources +- first area-boss victory may grant a one-time meaningful increase + +Do not finalize the complete 10–15-rank game-wide curve unless that has already been specified elsewhere. + +Only ensure the first region produces a sensible opening segment of that curve. + +--- + +## 10. Hunt Balancing + +Check encounter weights and player choice. + +The player should regularly see: + +- easy/reliable targets +- one more rewarding/riskier option +- occasional rare encounter + +Avoid hunts where all three cards are effectively identical. + +Rare Charred Raider should feel exciting but not so rare that normal play never demonstrates it. + +--- + +## 11. UI Polish + +Review all first-region screens against the established UI design. + +Important: + +- artwork remains dominant +- local view feels like a browser RPG, not a mobile card game +- current location is obvious +- NPCs/interactions are obvious +- bag capacity is visible when relevant +- reputation requirements are readable +- loot summary clearly distinguishes granted vs left-behind goods +- combat telegraphs are impossible to miss +- locked merchant items are understandable +- no German player-facing strings remain + +--- + +## 12. Content Consistency + +Verify names, keys and terminology across: + +- database seeds +- backend DTOs +- Angular components +- combat log +- item tooltips +- quest text +- merchant text +- location descriptions + +One entity must not appear as `Road Bandit` in one screen and `Street Raider` in another unless intentionally different. + +--- + +## 13. End-to-End Test Scenario + +Create at least one automated or documented repeatable E2E path: + +```text +1. Start with fresh character. +2. Visit South Gate Warden. +3. Accept Trouble Beyond the Gate. +4. Travel to Burned Road. +5. Hunt Ash Rat. +6. Receive first Ashen Pelt. +7. Hit HIDE capacity 1/1. +8. Return to warden. +9. Receive referral. +10. Visit Borin. +11. Receive Basic Hide Bag. +12. Collect five Ashen Pelts. +13. Complete quest. +14. Hunt/trade goods with Borin. +15. Gain Silver + regional reputation + World Renown. +16. Unlock/buy at least one progression offer. +17. Travel to Abandoned Watchpost. +18. Obtain stronger Tier-1 loot. +19. Discover Ash Pit. +20. Challenge Captain of the Ashen Band. +21. Win with a plausible non-BiS loadout. +22. Receive guaranteed relevant Tier-1 item. +23. Persist first-clear milestone. +``` + +The flow must survive page reloads and reconnects at sensible points. + +--- + +## 14. Technical Verification + +Before declaring Slice 0.12 complete: + +- all backend unit tests pass +- integration tests pass +- frontend tests pass +- production build passes +- migration path works from a clean database +- seed is idempotent where designed to be +- Docker production image still starts +- API and Angular static hosting still work together +- no client-authoritative combat/loot/reputation calculation has been introduced + +--- + +## 15. Definition of Done + +Slice 0.12 is complete when: + +- [ ] A fresh player can finish the complete Ashen Fields progression without manual DB edits. +- [ ] The first quest teaches the bag/merchant loop naturally. +- [ ] Burned Road, Abandoned Watchpost and Ash Pit each have a distinct purpose. +- [ ] Normal enemies reward goods/equipment, not direct XP/Silver/reputation. +- [ ] Merchant trade-in converts goods into Silver + reputation progression. +- [ ] Reputation visibly unlocks useful offers. +- [ ] Bags create a meaningful but not annoying return-trip rhythm. +- [ ] Tier-1 equipment progression is noticeable. +- [ ] Captain of the Ashen Band is a fair first-region climax. +- [ ] Boss guarantees meaningful progression loot. +- [ ] Combat duration targets are broadly met. +- [ ] Player-facing content is consistently English. +- [ ] The entire flow is server-authoritative for critical game state. +- [ ] Production build/tests are green. + +--- + +## 16. What Comes After 0.12 + +Only after this slice is stable should development expand into the next region. + +Recommended next phase: + +```text +Ashen Fields retrospective / balancing adjustments +→ next region foundation +→ new region enemies/mechanics +→ new reputation/economy content +``` + +Do not immediately add many MMO systems. + +The first question after 0.12 is: + +> **Is the repeatable loop of hunting, carrying, trading, upgrading and overcoming stronger content actually fun?** diff --git a/docs/playable-slices/0.6.6-English-Game-Content-Foundation.md b/docs/playable-slices/0.6.6-English-Game-Content-Foundation.md new file mode 100644 index 0000000..0a4fafe --- /dev/null +++ b/docs/playable-slices/0.6.6-English-Game-Content-Foundation.md @@ -0,0 +1,300 @@ +# Ashen Realms – Playable Slice 0.6.6 + +## English Game Content Foundation + +**Status:** Implementation Specification +**Depends on:** Slice 0.6.5 – Reputation / Renown Foundation +**Purpose:** Convert all currently implemented player-facing game content to English before the next major content expansion. + +--- + +## 1. Goal + +From this slice onward, **all player-facing game content is written in English**. + +Development documentation may remain German. Internal technical names may remain unchanged when they are already stable, but new data keys should be language-neutral English slugs. + +This slice is deliberately a cleanup and convention slice. It must not introduce a large localization framework. + +> Project rule after this slice: **All player-facing game content is English. Development documentation may remain German.** + +--- + +## 2. Why this slice exists now + +The project is about to add significantly more content: + +- more monsters +- trade goods +- loot bags +- merchants +- reputation-gated offers +- NPC dialogue +- quests +- the Abandoned Watchpost +- the Ash Pit +- the first area boss + +Translating later would touch much more persisted content, UI copy, seed data and tests. Therefore the language switch happens before Slice 0.7 V2. + +--- + +## 3. Scope + +Convert every **currently visible** German string in the playable loop: + +- navigation +- locations +- monsters +- items +- combat actions +- combat log +- hunt screen +- travel screen +- loot screen +- inventory +- character screen +- system messages +- danger labels +- error messages visible to the player + +Do **not** translate German development documentation in `/docs` unless it is itself rendered in the game. + +--- + +## 4. Naming convention + +Technical identity must not depend on display language. + +Preferred pattern: + +```ts +{ + key: 'ash-rat', + name: 'Ash Rat' +} +``` + +Avoid new language-specific identifiers such as: + +```ts +key: 'aschenratte' +``` + +Existing UUIDs must never change because of the language conversion. + +--- + +## 5. Initial terminology + +Use the following English terminology consistently for the currently relevant content. + +### Locations + +| German / Previous Label | English Player-Facing Label | Suggested Key | +|---|---|---| +| Graufurt | Graufurt | `graufurt` | +| Südtor von Graufurt | Graufurt South Gate | `graufurt-south-gate` | +| Verbrannte Straße | Burned Road | `burned-road` | +| Verlassener Wachtposten | Abandoned Watchpost | `abandoned-watchpost` | +| Aschengrube | Ash Pit | `ash-pit` | +| Aschenfelder | Ashen Fields | `ashen-fields` | + +`Graufurt` is treated as a proper place name and remains unchanged for now. + +### Monsters + +| German / Previous Label | English Label | Suggested Key | +|---|---|---| +| Aschenratte | Ash Rat | `ash-rat` | +| Verwilderter Straßenhund | Feral Road Hound | `feral-road-hound` | +| Straßenräuber | Road Bandit | `road-bandit` | +| Verkohlter Plünderer | Charred Raider | `charred-raider` | +| Plünderer-Späher | Raider Scout | `raider-scout` | +| Plünderer-Veteran | Raider Veteran | `raider-veteran` | +| Aschenwühler | Ash Burrower | `ash-burrower` | +| Verbrannter Jagdhund | Burned Hound | `burned-hound` | +| Hauptmann der Aschenbande | Captain of the Ashen Band | `ashen-band-captain` | + +### Core combat actions + +- Attack +- Heavy Strike +- Shield Bash +- Defend +- Potion +- Flee + +### Navigation + +- Map +- Hunt +- Quests +- Inventory +- Character +- Shop + +### Common UI + +- Current Location +- Travel +- Begin Travel +- Begin Hunt +- Search Again +- Attack +- Back +- Loot +- Equip +- Unequip +- Reputation +- World Renown +- Silver +- Required Reputation +- Bag Capacity + +### Danger labels + +- Weak +- Suitable +- Strong +- Very Dangerous +- Deadly + +--- + +## 6. Existing item names + +At minimum, currently seeded or visible Tier-1 items should use English display names. + +Recommended names: + +- Worn Shortsword +- Bandit Blade +- Ashen Blade +- Bandit Hood +- Reinforced Leather Jacket +- Plunderer Gloves +- Watchman's Leggings +- Ashen Boots +- Mark of the Border Watch +- Charred Captain's Pendant + +Do not rename stable item keys merely to make them prettier. Only display values should be changed unless the old key is clearly temporary and no persisted references depend on it. + +--- + +## 7. Combat log language + +All newly generated combat events must resolve to English text. + +Examples: + +```text +Round 2 +You use Shield Bash. +Road Bandit takes 12 damage. +Road Bandit's Heavy Strike is interrupted. +``` + +```text +Feral Road Hound bites you for 9 damage. +You are Bleeding for 2 rounds. +``` + +Combat event persistence should continue to prefer structured event data over storing fully rendered localized strings wherever possible. + +--- + +## 8. UI text architecture + +Do not build a full translation database in this slice. + +For static Angular UI text, it is acceptable to use centralized constants or a lightweight text map where useful. + +For persisted game content, keep: + +```text +key +name +shortDescription / description +``` + +with English values. + +A future localization layer may introduce translations without changing the content key. + +--- + +## 9. Error messages + +Domain error `code` values remain technical and stable. + +Example: + +```json +{ + "code": "BAG_CAPACITY_EXCEEDED", + "message": "You cannot carry any more hides." +} +``` + +The player-facing `message` is English. + +--- + +## 10. Migration / seed behavior + +If names and descriptions are stored in PostgreSQL: + +- update existing rows by stable `key` +- do not delete and recreate content rows unnecessarily +- preserve UUIDs +- preserve character inventory references +- keep migrations idempotent where appropriate + +If seed files own the content, update both the seed definitions and the existing database through a controlled migration/update step. + +--- + +## 11. Tests + +Add or update tests so that: + +- current location API returns English names +- hunt encounters return English monster names +- combat UI renders English action labels +- combat log contains English player-facing text +- inventory uses English item names +- no existing test expects German display text + +A simple development-only grep may be used to identify obvious German UI remnants, but it is not a substitute for actual runtime verification. + +--- + +## 12. Acceptance Criteria + +This slice is complete when: + +- [ ] The complete currently playable flow contains no intentional German player-facing text. +- [ ] Existing content IDs and player state remain valid. +- [ ] Locations, monsters and items use English display names. +- [ ] Combat actions and combat log are English. +- [ ] Navigation and common system messages are English. +- [ ] New stable content keys follow English/language-neutral slug conventions. +- [ ] No large localization framework was introduced. +- [ ] All existing tests pass. + +--- + +## 13. Explicitly Out of Scope + +Do not implement yet: + +- German/English language switch +- browser language detection +- translation management UI +- translation tables for every content entity +- Crowdin / Phrase / Lokalise integration +- automatic machine translation + +The only required runtime language after this slice is **English**. diff --git a/docs/playable-slices/0.7.5-Monster-Categories-and-Loot-Bags.md b/docs/playable-slices/0.7.5-Monster-Categories-and-Loot-Bags.md new file mode 100644 index 0000000..1ce1ee6 --- /dev/null +++ b/docs/playable-slices/0.7.5-Monster-Categories-and-Loot-Bags.md @@ -0,0 +1,321 @@ +# Ashen Realms – Playable Slice 0.7.5 + +## Monster Categories & Loot Bags + +**Status:** Implementation Specification +**Depends on:** Slice 0.7 V2 +**Purpose:** Introduce category-based carrying limits for monster trade goods and create the first logistical reason to return from hunting. + +--- + +## 1. Goal + +Monster loot should not be infinitely farmable in one trip. + +The player can carry only a small amount of category-specific trade goods without a bag. Dedicated loot bags increase that capacity. + +This creates the loop: + +**Hunt → fill bag → return → exchange goods → upgrade capacity → hunt longer.** + +The system must be simple, visible and data-driven. + +--- + +## 2. Core Rule + +Each trade good belongs to exactly one **Loot Category**. + +A character has a carrying capacity for each loot category. + +Without a matching bag, the default carrying capacity is: + +**1 unit per loot category.** + +Example: + +```text +Ashen Pelt: HIDE +Current HIDE capacity: 1 +Player owns 1 Ashen Pelt +→ another Ashen Pelt cannot be carried +``` + +A Hide Bag with capacity 5 changes this to: + +```text +HIDE capacity: 5 +``` + +--- + +## 3. Initial Categories + +Implement only the categories needed by the current content, but design the enum so future categories can be added. + +Initial categories: + +```text +HIDE +RAIDER_TROPHY +``` + +Suggested future categories, not required yet: + +```text +CHITIN +UNDEAD_RELIC +ARCANE_REMAINS +``` + +--- + +## 4. Initial Trade Good Mapping + +| Trade Good | Loot Category | +|---|---| +| Ashen Pelt | HIDE | +| Tough Hide | HIDE | +| Raider Insignia | RAIDER_TROPHY | +| Charred Raider Insignia | RAIDER_TROPHY | + +The mapping belongs to content data, not hardcoded monster-specific UI logic. + +--- + +## 5. Monster Categories + +Monster definitions also receive a broad gameplay category. + +Initial examples: + +```text +BEAST +HUMANOID +``` + +Suggested mapping: + +| Monster | Monster Category | +|---|---| +| Ash Rat | BEAST | +| Feral Road Hound | BEAST | +| Road Bandit | HUMANOID | +| Charred Raider | HUMANOID | + +Monster category and loot category are separate concepts. + +Example: different BEAST monsters can later drop HIDE, CHITIN or another loot category. + +Do not assume `monsterCategory === lootCategory`. + +--- + +## 6. Bag Model + +Loot bags are **not normal armor equipment slots**. + +They belong to a separate character loadout/state. + +Recommended data model concept: + +```text +LootBagDefinition +- id +- key +- name +- lootCategory +- capacity +- iconPath + +CharacterLootBag +- characterId +- lootBagDefinitionId +- equipped / active +``` + +Only one active bag per loot category is required for V1. + +No nesting and no physical inventory grid. + +--- + +## 7. Initial Bag Definitions + +The system must support at least: + +### Basic Hide Bag + +```text +Name: Basic Hide Bag +Category: HIDE +Capacity: 5 +``` + +### Basic Trophy Pouch + +```text +Name: Basic Trophy Pouch +Category: RAIDER_TROPHY +Capacity: 5 +``` + +They do not both need to be obtainable in this slice. Acquisition is handled by the merchant/quest slices. + +--- + +## 8. Capacity Calculation + +The server is the single authority. + +Conceptually: + +```text +capacity(category) = active bag capacity +if no active bag exists: + capacity(category) = 1 +``` + +The current amount is the sum of all owned trade-good quantities in that category. + +Equipment, consumables and normal items are not affected by loot-bag capacity. + +--- + +## 9. Full Bag Behavior + +If combat loot would exceed the capacity: + +- the combat victory remains valid +- the trade good is not granted beyond capacity +- other loot rolls still succeed normally +- equipment must not be lost because the hide bag is full +- the loot summary explains what was left behind + +Example: + +```text +Loot +Ashen Pelt ×1 – Left behind (Hide Bag full) +Bandit Hood ×1 – Added to inventory +``` + +Do not silently discard loot without player feedback. + +--- + +## 10. Mixed Loot + +If one result grants multiple trade goods, grant as many as fit. + +Example: + +```text +Current HIDE: 4 / 5 +Reward: Tough Hide ×2 +Granted: 1 +Left behind: 1 +``` + +The result DTO should make granted and rejected quantities explicit. + +--- + +## 11. API / DTO Requirements + +The frontend needs enough information to display current carrying state. + +Example DTO concept: + +```json +{ + "category": "HIDE", + "current": 4, + "capacity": 5, + "bag": { + "key": "basic-hide-bag", + "name": "Basic Hide Bag" + } +} +``` + +Provide a server-side way for the player UI to retrieve all relevant category capacities. + +This can be part of inventory state or a dedicated endpoint, whichever best fits the current codebase. + +--- + +## 12. UI Requirements + +The current carrying state must be visible during or directly after hunting. + +Minimum: + +```text +Hides 4 / 5 +Raider Trophies 1 / 1 +``` + +If a category is full, make it obvious before the player starts another farm cycle. + +The loot summary must display: + +- granted trade goods +- capacity after loot +- left-behind goods when full + +Do not turn this into a large inventory-management screen. + +--- + +## 13. Tests + +### Capacity + +- no bag gives capacity 1 +- Basic Hide Bag gives HIDE capacity 5 +- Hide Bag does not increase RAIDER_TROPHY capacity +- only active bag affects capacity + +### Loot granting + +- first Ashen Pelt can be carried without a bag +- second Ashen Pelt is rejected without a bag +- equipment reward still grants when trade-good capacity is full +- partial quantity grant works + +### Security + +- client cannot submit a fake capacity +- client cannot fake an equipped bag +- capacity is derived from persisted character state + +--- + +## 14. Acceptance Criteria + +- [ ] Monster definitions have a reusable monster category. +- [ ] Trade goods have a reusable loot category. +- [ ] Default capacity without bag is 1 per loot category. +- [ ] Dedicated loot bags increase only their configured category. +- [ ] Capacity enforcement is server-authoritative. +- [ ] Full bags never invalidate a combat victory. +- [ ] Excess trade goods are clearly shown as left behind. +- [ ] Equipment/consumables are unaffected by trade-good capacity. +- [ ] Current bag capacity can be displayed in the UI. +- [ ] Tests cover full, partial and no-bag cases. + +--- + +## 15. Out of Scope + +Do not implement: + +- weight +- Tetris inventory +- bag durability +- random bag affixes +- crafting bags +- upgrades on one physical bag item +- more than one active bag per category +- automatic sending of excess loot to storage diff --git a/docs/playable-slices/0.8-Graufurt-Merchant-and-Trade-In.md b/docs/playable-slices/0.8-Graufurt-Merchant-and-Trade-In.md new file mode 100644 index 0000000..4c226bc --- /dev/null +++ b/docs/playable-slices/0.8-Graufurt-Merchant-and-Trade-In.md @@ -0,0 +1,300 @@ +# Ashen Realms – Playable Slice 0.8 + +## Graufurt Merchant & Trade-In Loop + +**Status:** Implementation Specification +**Depends on:** Slice 0.7.5 +**Purpose:** Close the first new progression loop by allowing monster trade goods to be exchanged for Silver, regional reputation and World Renown. + +--- + +## 1. Goal + +For the first time, the player can turn hunting success into long-term progression. + +The loop becomes: + +**Hunt → carry trade goods → return to Graufurt → exchange goods → receive Silver + reputation + World Renown → prepare for the next trip.** + +Normal monster kills continue to give no direct money or reputation. + +--- + +## 2. Merchant + +Introduce one functional merchant in Graufurt. + +Working name: + +**Borin, Quartermaster** + +If an existing project NPC already fills this role, reuse that NPC instead of creating a duplicate. + +The merchant requires: + +- local-view/NPC interaction entry point +- dialogue/open-shop action +- trade-good exchange view +- basic shop view + +The UI must use English player-facing content. + +--- + +## 3. Merchant Roles + +Borin initially supports two separate operations: + +### Trade In + +The player hands over monster trade goods. + +The server grants configured rewards. + +### Shop + +The player can spend Silver on basic supplies/items already allowed by the current content scope. + +Reputation gating is introduced in Slice 0.8.5, not here. + +--- + +## 4. Trade-In Reward Model + +Each accepted trade good has a data-driven exchange definition. + +Recommended conceptual fields: + +```text +tradeGoodKey +merchantKey / regionKey +silverPerUnit +regionalReputationPerUnit +worldRenownPerUnit or batch rule +enabled +``` + +Exact values are balancing data and may remain provisional. + +The important architectural rule is: + +> The kill produces the object. The merchant converts the object into economic and reputation progression. + +--- + +## 5. Initial Exchangeable Goods + +At minimum: + +- Ashen Pelt +- Tough Hide +- Raider Insignia +- Charred Raider Insignia + +The rarer Charred Raider trade good should be worth visibly more than the basic Ashen Pelt. + +Do not hardcode reward logic in Angular. + +--- + +## 6. Reputation Effects + +A successful exchange can improve: + +- **regional reputation** for the current progression region +- **World Renown** as the global long-term progression value + +Use the reputation/renown system introduced before this slice. + +If the existing reputation implementation also tracks NPC-specific reputation, the exchange may optionally improve Borin's personal reputation, but the slice must not invent a second competing progression model. + +The exact reward numbers should be configurable. + +--- + +## 7. Silver + +Silver is now primarily earned through exchange and quests/services rather than directly from normal monsters. + +The merchant exchange must be one of the first reliable Silver sources. + +Silver is persisted server-side and must be granted in the same transaction as the removal of trade goods. + +--- + +## 8. Transaction Safety + +A trade-in is atomic. + +Conceptually: + +```text +validate character ownership +validate quantity +validate merchant accepts good +remove goods +add Silver +add regional reputation +add World Renown +persist exchange record if needed +commit +``` + +If any required step fails, no partial exchange is allowed. + +The same trade goods must never be redeemable twice. + +--- + +## 9. API + +Recommended operation: + +```text +POST /api/merchants/:merchantKey/trade-in +``` + +Example request: + +```json +{ + "items": [ + { "itemKey": "ashen-pelt", "quantity": 5 } + ] +} +``` + +Example response shape: + +```json +{ + "consumed": [ + { "itemKey": "ashen-pelt", "quantity": 5 } + ], + "rewards": { + "silver": 20, + "regionalReputation": 10, + "worldRenown": 2 + }, + "balances": { + "silver": 84, + "regionalReputation": 26, + "worldRenown": 4 + } +} +``` + +Numbers above are illustrative only. + +--- + +## 10. Trade-In UI + +The trade-in view should show: + +- trade good icon +- name +- quantity carried +- exchange value +- selected quantity +- resulting reward preview +- confirmation button + +Useful actions: + +- Trade Selected +- Trade All + +After completion, show a clear summary: + +```text +Trade Complete +5 Ashen Pelts handed in ++20 Silver ++10 Ashen Fields Reputation ++2 World Renown +``` + +Do not use a mobile-game reward explosion. Keep presentation consistent with the dark browser-RPG UI. + +--- + +## 11. Bag Interaction + +Trade-in removes items from the player's carrying total immediately. + +Example: + +```text +Hides before exchange: 5 / 5 +Trade 5 Ashen Pelts +Hides after exchange: 0 / 5 +``` + +This immediately frees bag capacity for another trip. + +--- + +## 12. Basic Shop + +The shop may initially sell only already-supported basics, such as: + +- Small Healing Potion +- simple starter equipment if desired + +Avoid adding many new items just to populate the shop. + +The important feature of this slice is **trade-in**, not shop breadth. + +--- + +## 13. Tests + +### Exchange + +- owned goods can be exchanged +- excessive quantity is rejected +- unsupported item is rejected +- trade removes exact quantity +- Silver is granted +- regional reputation is granted +- World Renown follows configured reward rule +- capacity becomes available after goods are removed + +### Atomicity + +- reward failure does not consume goods +- duplicate request cannot duplicate rewards if request/idempotency protection exists in current architecture + +### Regression + +- normal monster victory still grants no Silver/reputation directly + +--- + +## 14. Acceptance Criteria + +- [ ] Graufurt has an interactable merchant entry point. +- [ ] Player can view carried trade goods accepted by the merchant. +- [ ] Player can exchange selected quantities. +- [ ] Trade goods are removed server-side. +- [ ] Silver is granted server-side. +- [ ] Regional reputation is granted according to configured values. +- [ ] World Renown is granted according to configured values/rules. +- [ ] Bag capacity is freed by trade-in. +- [ ] Exchange is transactional and cannot be duplicated by client manipulation. +- [ ] The complete Hunt → Return → Trade loop works without DB editing. + +--- + +## 15. Out of Scope + +Do not add yet: + +- reputation-locked shop items +- referral-based merchant exceptions +- first tutorial quest +- advanced buy/sell economy +- player-to-player trading +- auction house +- crafting materials market diff --git a/docs/playable-slices/0.8.5-Reputation-Gated-Merchant-Offers.md b/docs/playable-slices/0.8.5-Reputation-Gated-Merchant-Offers.md new file mode 100644 index 0000000..a36acc9 --- /dev/null +++ b/docs/playable-slices/0.8.5-Reputation-Gated-Merchant-Offers.md @@ -0,0 +1,247 @@ +# Ashen Realms – Playable Slice 0.8.5 + +## Reputation-Gated Merchant Offers + +**Status:** Implementation Specification +**Depends on:** Slice 0.8 +**Purpose:** Make reputation visible and useful by locking selected merchant offers behind reputation or explicit unlock conditions instead of character levels. + +--- + +## 1. Goal + +The reputation system must change what the player can do. + +A merchant should be able to communicate: + +> "I don't know you well enough for that." + +or: + +> "Earn more trust in the Ashen Fields first." + +The player sees desirable locked items, understands the requirement, and has a reason to continue hunting and trading. + +--- + +## 2. No Level-Gated Merchant Progression + +New merchant offers must not use character level as the primary progression gate. + +Preferred gates: + +- regional reputation +- World Renown +- NPC reputation, if already supported +- quest/unlock flag + +The server is authoritative for all purchase requirements. + +If `requiredLevel` still exists in an old item schema, it may remain for compatibility, but new Tier-1 merchant progression in these slices should not rely on it. + +--- + +## 3. Offer Requirement Model + +Merchant offers should support an optional requirement definition. + +Conceptual examples: + +```text +NONE +REGION_REPUTATION +WORLD_RENOWN +NPC_REPUTATION +QUEST_FLAG +``` + +An offer may later support multiple requirements, but do not overbuild a general rule engine unless the current code already has one. + +A pragmatic structure is enough: + +```text +requiredRegionReputation +requiredWorldRenown +requiredNpcReputation +requiredUnlockFlag +``` + +with nullable values. + +--- + +## 4. Initial Locked Offers + +At least two useful offers should demonstrate the system. + +Recommended examples: + +### Basic Hide Bag + +- important progression item +- may be normally reputation-gated +- Slice 0.9 can temporarily bypass the gate through a quest referral + +### Basic Trophy Pouch + +- unlocked after the player has demonstrated some regional reputation + +Optional additional offer: + +### Bandit Blade or another Tier-1 gap filler + +- available at a higher reputation threshold + +Exact thresholds are balancing data. + +--- + +## 5. Visible Locked Offers + +Locked offers should usually remain visible. + +Example: + +```text +Basic Trophy Pouch +Capacity: 5 Raider Trophies +Price: 40 Silver +Requires: Ashen Fields Reputation 25 +Current: 14 +``` + +The Buy action is disabled. + +This is preferable to hiding every locked item, because visible rewards create goals. + +--- + +## 6. Server Validation + +A malicious client must not bypass a disabled button. + +Purchase flow: + +```text +load character +load merchant offer +validate price +validate reputation requirement +validate quest/unlock flag +validate inventory/bag ownership rules +remove currency +grant item/bag +commit +``` + +If reputation is insufficient, return a stable domain error such as: + +```text +MERCHANT_REPUTATION_TOO_LOW +``` + +with an English player-facing message. + +--- + +## 7. Referral / Exception Support + +Slice 0.9 needs a special tutorial moment: + +The merchant normally would not give the player a useful bag yet, but a gate NPC sends the player with a referral. + +Therefore the offer/unlock system must support one minimal exception: + +```text +requiredUnlockFlag = referred-by-south-gate-warden +``` + +or an equivalent quest reward/grant path. + +Do not implement a complex faction favor engine. + +The requirement is simply that the quest can legally allow one specific acquisition that reputation alone would not yet allow. + +--- + +## 8. UI Requirements + +Merchant cards/rows show: + +- item/bag name +- icon +- Silver price +- relevant effect +- requirement +- player's current value +- locked/unlocked state + +Use clear English copy. + +Examples: + +```text +Requires Ashen Fields Reputation 25 +``` + +```text +Requires World Renown 3 +``` + +```text +Unavailable – Borin does not know you well enough. +``` + +--- + +## 9. Feedback on Unlock + +When a reputation increase makes an offer newly available, the player should receive lightweight feedback. + +Example: + +```text +New merchant offer unlocked: Basic Trophy Pouch +``` + +No large modal is required. + +--- + +## 10. Tests + +- offer with no requirement can be purchased +- insufficient regional reputation blocks purchase +- sufficient regional reputation allows purchase +- insufficient World Renown blocks purchase +- quest flag can unlock configured tutorial offer +- disabled client state is not trusted by backend +- price is still required even when reputation condition is met +- unlock state changes after a successful trade-in raises reputation + +--- + +## 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. + +--- + +## 12. Out of Scope + +Do not implement: + +- dynamic merchant personalities +- haggling +- randomized daily shops +- faction wars +- reputation decay +- negative reputation systems +- multiple currencies per individual offer unless already required diff --git a/docs/playable-slices/0.9-First-Quest-and-Bag-Tutorial.md b/docs/playable-slices/0.9-First-Quest-and-Bag-Tutorial.md new file mode 100644 index 0000000..244607a --- /dev/null +++ b/docs/playable-slices/0.9-First-Quest-and-Bag-Tutorial.md @@ -0,0 +1,321 @@ +# Ashen Realms – Playable Slice 0.9 + +## First Quest & Loot-Bag Tutorial + +**Status:** Implementation Specification +**Depends on:** Slice 0.8.5 +**Purpose:** Introduce quests, NPC-directed progression, bag capacity and the merchant loop through one small coherent tutorial chain. + +--- + +## 1. Goal + +This slice should teach several systems without a conventional tutorial popup sequence. + +The player learns naturally that: + +- NPCs provide direction +- monster goods must be carried +- carrying capacity matters +- merchants can help +- reputation affects relationships/offers +- hunting goods can later be exchanged for progression + +The quest should feel like world interaction, not a checklist tutorial. + +--- + +## 2. Quest Concept + +Working quest title: + +**Trouble Beyond the Gate** + +Quest-giver: + +**South Gate Warden** + +Use an existing named gate NPC if one has already been implemented. Do not create a duplicate NPC merely because this document uses a generic title. + +Merchant: + +**Borin, Quartermaster** or the existing Graufurt merchant. + +--- + +## 3. Narrative Flow + +### Step 1 – Speak with the South Gate Warden + +The warden is concerned about increasingly aggressive scavengers and raiders beyond the gate. + +Player-facing English example: + +> "The road has gone bad. Start small. Bring me five Ashen Pelts from the rats beyond the gate. I want to know what the ash is doing to them." + +Objective: + +```text +Collect 5 Ashen Pelts +``` + +--- + +## 4. Capacity Problem + +The player currently has no Hide Bag. + +Default HIDE capacity is 1. + +After collecting the first Ashen Pelt: + +```text +Ashen Pelts 1 / 1 +Hide capacity reached. +``` + +The quest remains at `1 / 5`. + +The game should now provide a clear next action rather than leaving the player confused. + +Suggested quest update: + +```text +You cannot carry enough pelts. Return to the South Gate Warden. +``` + +Do not secretly increase capacity for the quest. + +--- + +## 5. Referral to the Merchant + +On returning, the warden acknowledges the problem and refers the player to Borin. + +Example: + +> "Right. You're not equipped for hauling spoils yet. Go see Borin in Graufurt. Tell him I sent you. He'll complain, but he'll give you something useful." + +Set a persisted unlock/referral flag, for example: + +```text +referred-by-south-gate-warden +``` + +New objective: + +```text +Speak with Borin in Graufurt +``` + +--- + +## 6. Merchant Interaction + +Borin normally would not provide the bag at the player's current reputation. + +His dialogue should make the relationship system visible. + +Example: + +> "I don't know you, and I don't hand out gear to every wanderer who walks in here." + +Then after recognizing the referral: + +> "But the South Gate Warden sent you. Fine. Take this. Bring it back full and make it worth my trouble." + +Grant: + +**Basic Hide Bag** + +```text +Category: HIDE +Capacity: 5 +``` + +The grant should use the quest/referral exception supported by Slice 0.8.5. + +--- + +## 7. Continue the Hunt + +The player can now carry five HIDE-category goods. + +Objective returns to: + +```text +Collect 5 Ashen Pelts +``` + +Important: + +- an already-owned first pelt still counts +- the player should need four more, not five more +- the bag capacity view changes immediately to `1 / 5` + +--- + +## 8. Quest Turn-In + +The player returns to the South Gate Warden with five Ashen Pelts. + +The quest consumes the required five pelts. + +The warden explains the normal long-term loop: + +> "Good. That's enough for me. From now on, take hides and trophies to Borin. He'll pay for useful spoils, and word gets around when you keep the roads clear." + +This line connects: + +- monster loot +- merchant exchange +- Silver +- regional reputation +- World Renown + +without needing a separate tutorial screen. + +--- + +## 9. Quest Rewards + +The main reward of this quest is the **Basic Hide Bag** and system knowledge. + +Optional one-time rewards may include: + +- small regional reputation increase +- small World Renown milestone increase +- quest completion flag + +Avoid a large Silver reward that undermines the merchant trade loop. + +No XP reward. + +--- + +## 10. Minimal Quest System Requirements + +The system must support at least: + +- quest definition +- quest acceptance +- ordered objectives +- item collection progress +- NPC interaction objective +- persisted completion +- quest flags/unlock flags +- item consumption on turn-in +- one-time rewards + +Do not build a full branching narrative engine. + +A simple step-based quest state machine is sufficient. + +--- + +## 11. Quest State Safety + +The flow must not softlock when: + +- player already owns one or more Ashen Pelts before accepting +- player sells/trades pelts while quest is active +- player disconnects during a quest step +- bag was already obtained through development data +- player tries to receive the referral bag twice + +Define deterministic behavior. + +Recommended: + +- objectives derive progress from current owned quantity where appropriate +- quest item consumption validates quantity at turn-in +- bag grant is idempotent +- referral flag persists + +--- + +## 12. UI Requirements + +At minimum: + +### NPC / Local View + +- quest available marker +- quest in progress marker +- quest turn-in marker +- Talk interaction + +### Quest UI + +Show: + +- title +- short description +- current objective +- progress + +Example: + +```text +Trouble Beyond the Gate +Collect Ashen Pelts 1 / 5 + +You cannot carry enough pelts. Return to the South Gate Warden. +``` + +### Bag UI + +When the bag is granted: + +```text +New Loot Bag +Basic Hide Bag +Hide Capacity: 5 +``` + +--- + +## 13. Tests + +- quest can be accepted only once in active state +- first pelt updates objective to 1/5 +- no bag blocks carrying second HIDE item +- return step activates correctly +- referral flag is set +- merchant grants Basic Hide Bag once +- capacity becomes 5 +- previously owned pelt remains counted +- five pelts allow turn-in +- turn-in consumes five pelts +- quest completes and cannot reward bag repeatedly + +--- + +## 14. Acceptance Criteria + +- [ ] One complete NPC quest chain is playable. +- [ ] The quest intentionally exposes the default 1-item carrying limit. +- [ ] The player is directed back instead of softlocked. +- [ ] A persisted merchant referral is created. +- [ ] Merchant grants Basic Hide Bag because of the referral. +- [ ] Hide capacity becomes 5. +- [ ] Player can collect five Ashen Pelts. +- [ ] Turn-in consumes the pelts and completes the quest. +- [ ] NPC explains that future goods should be traded to the merchant. +- [ ] Quest grants no XP. +- [ ] All dialogue/UI text is English. + +--- + +## 15. Out of Scope + +Do not implement: + +- branching quest choices +- voice acting +- cinematic dialogue +- daily quests +- repeatable bounty framework +- quest sharing +- party quest progress +- large quest journal taxonomy diff --git a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.11_Grenzmarken_First_Merchant.md b/docs/playable-slices/Ashen_Realms_Playable_Slice_0.11_Grenzmarken_First_Merchant.md deleted file mode 100644 index c51ca0c..0000000 --- a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.11_Grenzmarken_First_Merchant.md +++ /dev/null @@ -1,283 +0,0 @@ -# Ashen Realms – Playable Slice 0.11: Grenzmarken & First Merchant - -**Status:** Ready for implementation -**Prerequisite:** Playable Slice 0.10 – First NPC & Quest -**Scope:** First regional currency and targeted merchant progression -**Currency:** Grenzmarken -**Next Slice:** Playable Slice 0.12 – Aschenfelder Complete - -## 1. Goal - -Implement the first anti-frustration progression system. - -The core philosophy is: - -> Drops create excitement. Regional currency prevents frustration. - -The player can earn Grenzmarken through normal progression and use them to buy targeted Tier-1 upgrades. - -## 2. Grenzmarken - -Implement persistent: - -```text -Grenzmarken -``` - -Sources include existing configured rewards such as: - -```text -Quests -rare enemies -stronger enemies -Elite -Boss -``` - -Use the current balancing/content definitions as the source of truth. - -## 3. Currency model - -At this stage a reusable currency model may become worthwhile. - -Conceptually: - -```text -CurrencyDefinition -CharacterCurrency -``` - -Example stable keys: - -```text -silver -border-marks -``` - -If Silver is already safely represented directly on Character, do not force a risky large migration solely for architectural purity. - -The important requirement is that Grenzmarken are persistent and server-authoritative. - -## 4. Merchant - -Introduce the first regional merchant at the appropriate existing NPC/location. - -Use existing project content if a merchant NPC/location has already been defined. - -Minimal merchant data: - -```text -ShopDefinition -ShopOffer -``` - -A ShopOffer should determine: - -```text -item -currency -price -availability -``` - -## 5. Grenzmarken offers - -Use the established prices: - -| Item | Price | -|---|---:| -| Räuberhaube | 5 Grenzmarken | -| Plündererhandschuhe | 6 Grenzmarken | -| Wachmannsbeinkleid | 8 Grenzmarken | -| Verstärkte Lederjacke | 10 Grenzmarken | -| Aschenklinge | 15 Grenzmarken | - -Do not silently rebalance these prices in this implementation slice. - -## 6. Shop API - -Use the existing REST conventions. - -Conceptually: - -```http -GET /api/shops/:shopId -POST /api/shops/:shopId/purchases -``` - -Purchase request: - -```json -{ - "offerId": "uuid" -} -``` - -Do not accept authoritative client values such as: - -```text -price -itemId -currency amount -discount -``` - -The server derives them from the persisted offer. - -## 7. Purchase transaction - -A purchase must be atomic: - -```text -load character currency -↓ -load and validate offer -↓ -validate balance -↓ -subtract Grenzmarken -↓ -create CharacterItem -↓ -commit -``` - -If the transaction fails, neither the currency deduction nor the item grant should remain partially applied. - -## 8. Ownership and duplicate items - -Purchased equipment becomes normal persistent CharacterItem data. - -Duplicates are allowed. - -Do not implement: - -```text -salvaging -buyback -duplicate conversion -pity conversion -``` - -## 9. Merchant UI - -Show: - -- merchant/NPC presentation -- current Grenzmarken balance -- available offers -- item icon -- item name -- item stats -- price -- affordability -- purchase action - -Reuse the item display and comparison language from Inventory where possible. - -## 10. Purchase feedback - -After purchase: - -```text -Grenzmarken balance decreases -↓ -item appears in inventory -↓ -player may open inventory -↓ -player may equip item -``` - -Do not automatically equip purchased equipment. - -## 11. Progression proof - -The important flow is: - -```text -desired drop does not appear -↓ -player keeps playing -↓ -Grenzmarken accumulate -↓ -merchant provides targeted upgrade -``` - -The player should always feel that unlucky drops still produce progress. - -## 12. Server authority - -The server decides: - -- currency balance -- reward grants -- offer availability -- offer price -- purchase validity -- resulting item ownership - -Angular only requests purchase of a valid server-provided `offerId`. - -## 13. Explicit non-goals - -Do not implement: - -```text -selling -buyback -dynamic prices -limited stock -shop refresh timers -player trading -auction house -discount systems -reputation -crafting vendor -``` - -## 14. Required tests - -Verify: - -- Grenzmarken rewards persist -- current balance is server-authoritative -- shop returns persisted offers -- insufficient balance rejects purchase -- valid purchase deducts exact configured cost -- valid purchase grants the correct CharacterItem -- purchase is transactional -- repeated request cannot accidentally duplicate a single transaction through race conditions -- item survives refresh -- purchased item can be equipped using existing equipment flow - -## 15. Definition of Done - -The player can: - -```text -earn Grenzmarken -↓ -see balance -↓ -open merchant -↓ -inspect offers -↓ -buy targeted Tier-1 item -↓ -item enters inventory -↓ -equip item -``` - -Random loot and guaranteed long-term progression are now connected. - -## 16. Handoff - -Next: - -```text -Playable Slice 0.12 – Aschenfelder Complete -``` diff --git a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.12_Aschenfelder_Complete.md b/docs/playable-slices/Ashen_Realms_Playable_Slice_0.12_Aschenfelder_Complete.md deleted file mode 100644 index b3981d2..0000000 --- a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.12_Aschenfelder_Complete.md +++ /dev/null @@ -1,490 +0,0 @@ -# Ashen Realms – Playable Slice 0.12: Aschenfelder Complete - -**Status:** Final integration slice for the first region -**Prerequisite:** Playable Slices 0.1–0.11 -**Scope:** Integration, balancing, content completion and polish for the Aschenfelder -**Primary Goal:** Complete the first genuinely playable regional progression loop. - -## 1. Goal - -Playable Slice 0.12 does not introduce a large new system. - -It integrates, verifies, balances, and polishes everything built so far into one coherent first region. - -The full intended flow is: - -```text -Graufurt -↓ -Südtor -↓ -Verbrannte Straße -↓ -first hunts -↓ -first combat -↓ -first loot -↓ -first upgrades -↓ -Verlassener Wachtposten -↓ -NPC / Quest -↓ -stronger enemies -↓ -first Elite -↓ -Grenzmarken -↓ -targeted upgrades -↓ -Aschengrube -↓ -regional Boss -↓ -guaranteed boss loot -↓ -path toward Dämmerwald discovered -``` - -## 2. Complete locations - -The Aschenfelder progression contains: - -```text -Südtor von Graufurt -Verbrannte Straße -Verlassener Wachtposten -Aschengrube -``` - -Each location must have a clear purpose and correct travel connections. - -## 3. Complete Tier-1 enemy set - -The relevant first-region pool should now include the established enemies: - -```text -Aschenratte -Verwilderter Straßenhund -Straßenräuber -Plünderer-Späher -Plünderer-Veteran -Verkohlter Plünderer -Aschenwühler -Verbrannter Jagdhund -Plündererhauptmann / Elite -Hauptmann der Aschenbande / Boss -``` - -If final naming differs in existing project content, preserve the existing finalized names rather than creating duplicates. - -## 4. Complete Tier-1 item set - -The relevant Tier-1 pool should now include: - -```text -Abgenutztes Kurzschwert -Räuberklinge -Aschenklinge -Räuberhaube -Verstärkte Lederjacke -Plündererhandschuhe -Wachmannsbeinkleid -Aschenstiefel -Zeichen der Grenzwacht -Anhänger des verbrannten Hauptmanns -``` - -Not every item must be required for progression. - -Prestige/special drops remain optional. - -## 5. Combat duration targets - -During the balancing pass, verify: - -### Normal enemies - -```text -4–8 rounds -``` - -### Elite - -```text -6–10 rounds -``` - -### Boss - -```text -8–14 rounds -``` - -These are balancing targets rather than hard rules. - -## 6. Power progression targets - -Use the established Combat Power progression as a reference: - -### Start - -```text -approximately 47 CP -``` - -### After first upgrades - -```text -approximately 60–70 CP -``` - -### Wachtposten progression - -```text -approximately 80–90 CP -``` - -### Aschengrube / boss-ready range - -```text -approximately 100–110 CP -``` - -Combat Power is an internal balancing tool, not a hard access gate. - -## 7. Progression pacing - -A typical player should need roughly: - -```text -12–18 normal combats -+ -quests -+ -boss -``` - -to progress meaningfully through the first region. - -The intended experience must not require 50+ normal combats for basic progression. - -## 8. First upgrade pacing - -Verify that within approximately the first: - -```text -3–5 combats -``` - -the player is very likely to see a meaningful equipment upgrade. - -If this does not happen reliably enough, tune only the smallest necessary variables: - -```text -drop rate -quest reward -starter equipment -``` - -Do not add unnecessary new reward systems. - -## 9. Boss readiness - -The regional boss should become reasonably beatable at approximately: - -```text -70–80% of maximum realistically obtainable regional power -``` - -Best-in-slot equipment should provide comfort and completionist value, not be mandatory. - -## 10. Soft-gate validation - -The player should not be blocked primarily by arbitrary level errors. - -Where possible: - -```text -actual world danger -``` - -should communicate that the player is too weak. - -A player may attempt difficult content early, but the game should make the risk clear. - -## 11. Full UI consistency pass - -Review all implemented screens together: - -```text -World -Hunt -Combat -Loot -Inventory -Quests -Merchant -``` - -Verify consistency of: - -```text -Topbar -SideNavigation -Footer -Context Panel -Panel frames -Buttons -DangerBadges -Item icons -Combat actions -Typography -Spacing -Design tokens -``` - -No screen should feel like a separate web application. - -## 12. Loading and error-state pass - -All core player actions need intentional UI states: - -```text -loading -success -domain error -network error -retry -``` - -Review at minimum: - -```text -Travel -Hunt -Combat Action -Reward -Equip -Quest -Purchase -``` - -Do not use browser alerts. - -## 13. Refresh and recovery pass - -The following states must survive browser refresh through authoritative backend state: - -```text -active travel -current hunt -active combat -finished combat/reward -inventory -equipment -active quest -Grenzmarken -boss completion -``` - -No critical gameplay state may exist only in Angular memory. - -## 14. Full integration test - -Where practical, automate the first complete progression flow: - -```text -character starts at Südtor -↓ -travel to Verbrannte Straße -↓ -hunt -↓ -combat -↓ -win -↓ -receive loot -↓ -equip upgrade -↓ -travel to Wachtposten -↓ -accept quest -↓ -defeat relevant enemies -↓ -progress quest -↓ -earn Grenzmarken -↓ -buy targeted item -↓ -equip item -↓ -travel to Aschengrube -↓ -defeat boss -↓ -receive guaranteed boss reward -↓ -region completion persists -``` - -## 15. Manual playtest – average RNG - -Perform at least one normal playthrough and measure: - -```text -combat duration -loot frequency -upgrade frequency -number of hunts -travel flow -quest pacing -Grenzmarken income -boss readiness -total region time -``` - -## 16. Manual playtest – bad RNG - -Test a deliberately unlucky run. - -Verify: - -```text -Grenzmarken still provide progress -progression does not hard-block -boss readiness remains achievable -player does not require extreme grind -``` - -## 17. Manual playtest – good RNG - -Test a deliberately lucky run. - -Verify: - -```text -region does not become instantly trivial -progression cannot be skipped too aggressively -boss remains meaningful -``` - -## 18. Data/content validation - -Verify: - -- all locations use persisted content -- all encounter pools use persisted relationships -- all loot is data-driven -- all shop offers are data-driven -- quest definitions are persisted/data-driven -- no core gameplay path depends on hardcoded Angular content -- no server domain service contains unnecessary location-specific special cases - -## 19. Server-authority validation - -Confirm that the backend remains authoritative for: - -```text -travel -hunt availability -encounter generation -danger rating -combat -loot -XP -Silver -Grenzmarken -inventory ownership -equipment -effective stats -quest progress -shop purchases -boss completion -``` - -## 20. Performance / technical cleanup - -Review the accumulated implementation for: - -- duplicate API calls -- duplicated frontend state -- duplicated domain validation -- unused temporary placeholder logic -- obsolete Slice 0.1–0.5 shortcuts -- hardcoded demo combat stats that should now use CharacterStatsService -- temporary combat placeholder routes -- duplicated item definitions -- migration quality -- seed idempotency - -Do not perform unrelated architectural rewrites. - -## 21. Definition of Done - -Playable Slice 0.12 / Aschenfelder Complete is complete when a fresh character can, without developer intervention: - -```text -start -↓ -travel -↓ -hunt -↓ -fight -↓ -receive loot -↓ -improve equipment -↓ -meet NPC -↓ -complete quest -↓ -fight Elite -↓ -earn Grenzmarken -↓ -buy targeted item -↓ -reach Aschengrube -↓ -defeat regional Boss -↓ -receive guaranteed progression reward -↓ -discover path toward Dämmerwald -``` - -## 22. Final product test - -The most important qualitative result is: - -> After completing the Aschenfelder, the player should want to see what waits in the Dämmerwald. - -If the loop is technically correct but does not create that motivation, the region still needs iteration. - -## 23. After Slice 0.12 - -Do not immediately expand into the full Dämmerwald. - -First: - -```text -playtest -measure -balance -fix friction -validate reward pacing -validate combat decisions -validate boss readiness -``` - -Only after the first region loop works well should the same systems be expanded into the Dämmerwald and later the Vergessene Ruinen. diff --git a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.7_Complete_Verbrannte_Strasse.md b/docs/playable-slices/Ashen_Realms_Playable_Slice_0.7_Complete_Verbrannte_Strasse.md deleted file mode 100644 index 2d00f9f..0000000 --- a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.7_Complete_Verbrannte_Strasse.md +++ /dev/null @@ -1,235 +0,0 @@ -# Ashen Realms – Playable Slice 0.7: Complete Verbrannte Straße - -**Status:** Ready for implementation -**Prerequisite:** Playable Slice 0.6 – Full First Combat -**Scope:** First fully playable hunting location -**Primary Location:** Verbrannte Straße -**Next Slice:** Playable Slice 0.8 – Wachtposten & First Elite - -## 1. Goal - -Turn the technical hunting location into the first complete gameplay location. - -The Verbrannte Straße must support repeated hunting, multiple enemy archetypes, differentiated mechanics, meaningful loot, and the first visible progression through equipment. - -## 2. Complete encounter pool - -The hunting pool becomes: - -```text -Aschenratte -Verwilderter Straßenhund -Straßenräuber -Verkohlter Plünderer – rare -``` - -## 3. Aschenratte - -Role: - -- Level 1. -- Basic enemy. -- No special mechanic required. - -Rewards: - -```text -4–7 Silver -8 XP -60% Aschenfell / trade material -8% simple starter-slot equipment -``` - -## 4. Verwilderter Straßenhund - -Role: - -- Level 1–2. -- Introduces the first simple status effect. - -Mechanic: - -```text -BLEED -``` - -Keep Bleed deliberately simple: - -- fixed duration -- fixed damage -- no complex stacking -- server-authoritative -- represented through structured combat state/events - -Rewards: - -```text -6–10 Silver -12 XP -70% Zähes Fell -8% Aschenstiefel -5% Kleiner Heiltrank -``` - -Do not implement the entire long-term consumable economy merely because a potion can drop. - -## 5. Straßenräuber - -Role: - -- Level 2. -- Reinforces Telegraphing. - -Mechanic: - -```text -prepared Heavy Attack -``` - -Rewards: - -```text -9–15 Silver -16 XP -18% Räuberklinge -12% Räuberhaube -8% Plündererhandschuhe -10% Kleiner Heiltrank -``` - -## 6. Verkohlter Plünderer - -Role: - -- Level 3. -- Rare encounter. -- Noticeably stronger. -- Early long-term target. - -Guaranteed: - -```text -25–35 Silver -35 XP -1 Grenzmarke -``` - -Additional equipment drops use the existing balancing/content definitions. - -The encounter should communicate: - -> This enemy may be too strong right now, but the player can return later. - -## 7. Encounter pool weights - -All encounter probability belongs in persisted `LocationMonster` content. - -Do not hardcode location-specific monster probabilities inside the HuntingService. - -Conceptually: - -```text -Aschenratte common -Straßenhund common -Straßenräuber normal -Verkohlter Plünderer rare -``` - -Exact weights may be tuned later. - -## 8. Grenzmarken - -Slice 0.7 may begin persisting Grenzmarken as reward data because the rare enemy already grants one. - -They do not need to be spendable yet. - -Spending Grenzmarken belongs to Slice 0.11. - -## 9. Progression target - -Within approximately the first 3–5 combats, the player should have a high chance of seeing the first meaningful equipment upgrade. - -The purpose is to prove: - -```text -hunt -→ fight -→ loot -→ equip -→ become stronger -``` - -through repeated play at one location. - -## 10. Hunt UI - -The Hunt context panel should now communicate the broader enemy pool. - -Example: - -```text -Mögliche Begegnungen - -Aschenratte -Verwilderter Straßenhund -Straßenräuber -??? -``` - -A rare enemy may remain hidden until first encountered. - -Do not expose exact encounter percentages. - -## 11. Explicit non-goals - -Do not implement yet: - -```text -Quest -NPC -Wachtposten progression -Boss -Merchant -Grenzmarken shop -Area completion -``` - -## 12. Required tests - -Verify: - -- all four monsters are persisted content -- all four are assigned to `burned-road` -- weighted selection remains deterministic in tests -- rare encounter selection is supported -- Bleed resolves correctly -- Straßenräuber Telegraphing still works -- loot tables match configured content -- Grenzmarken can be persisted when granted -- item upgrades remain persistent after reward flow - -## 13. Definition of Done - -The player can remain on the Verbrannte Straße and experience: - -```text -different encounters -↓ -different mechanics -↓ -different rewards -↓ -first upgrades -↓ -visibly increasing strength -``` - -The location now feels like a real piece of the game rather than a technical test area. - -## 14. Handoff - -Next: - -```text -Playable Slice 0.8 – Wachtposten & First Elite -``` diff --git a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.8_Wachtposten_First_Elite.md b/docs/playable-slices/Ashen_Realms_Playable_Slice_0.8_Wachtposten_First_Elite.md deleted file mode 100644 index ef3398f..0000000 --- a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.8_Wachtposten_First_Elite.md +++ /dev/null @@ -1,238 +0,0 @@ -# Ashen Realms – Playable Slice 0.8: Wachtposten & First Elite - -**Status:** Ready for implementation -**Prerequisite:** Playable Slice 0.7 – Complete Verbrannte Straße -**Scope:** First progression inside a region and first elite challenge -**Primary Location:** Verlassener Wachtposten -**Next Slice:** Playable Slice 0.9 – Aschengrube & First Boss - -## 1. Goal - -Introduce the first meaningful progression from one dangerous location to another. - -The player should reach a new location, encounter stronger enemies, discover an elite target, improve equipment, and return strong enough to defeat it. - -## 2. New location - -Add: - -```text -Verlassener Wachtposten -``` - -Connection: - -```text -Verbrannte Straße -↔ -Verlassener Wachtposten -``` - -Travel duration: - -```text -approximately 15 seconds -``` - -Travel remains server-authoritative. - -## 3. Location functions - -The Wachtposten should support: - -```text -Jagd beginnen -Wachtposten untersuchen -NPC visible -travel back to Verbrannte Straße -future travel toward Aschengrube -``` - -The NPC may already be visually present, but full NPC/quest interaction is deferred to Slice 0.10. - -## 4. Encounter pool - -Use the existing world/content design as the source of truth. - -Core enemies: - -```text -Straßenräuber -Plünderer-Späher -Plünderer-Veteran -Plündererhauptmann -``` - -## 5. Plünderer-Späher - -Role: - -- lighter humanoid enemy -- familiar combat foundation -- part of Wachtposten progression - -Use persisted content and data-driven mechanics. - -## 6. Plünderer-Veteran - -Role: - -- first clearly tougher regular humanoid -- combines existing mechanics - -Mechanics: - -```text -Heavy Attack -+ -defensive stance -``` - -The defensive stance may temporarily increase armor or otherwise use the established deterministic defensive mechanic. - -Avoid adding a generic scripting engine. - -## 7. Plündererhauptmann - -Role: - -```text -ELITE -``` - -The elite combines 2–3 already learned mechanics. - -The elite should be: - -- difficult or impractical for an unupgraded starting character -- manageable after meaningful Tier-1 upgrades -- farmable for a recognizable desirable reward - -## 8. Elite combat duration - -Target: - -```text -6–10 rounds -``` - -This is a balancing goal, not a hard mechanical rule. - -## 9. Elite loot - -The elite must have a clear reason to be farmed. - -Use the existing balancing/content data for its target rewards. - -Do not invent a separate random loot pool if the project already defines the relevant Tier-1 upgrade source. - -A desirable weapon such as the Aschenklinge may serve as a primary target where consistent with existing content. - -## 10. Danger Rating - -By this slice, danger ratings should use real effective player stats. - -The backend should derive them from: - -```text -CharacterStatsService -+ -enemy / encounter power -``` - -Supported ratings: - -```text -WEAK -MATCH -STRONG -VERY_DANGEROUS -DEADLY -``` - -Use the balancing thresholds as the initial calibration. - -Do not calculate this in Angular. - -## 11. Encounter UI - -Elite encounters must remain part of the reusable EncounterCard system. - -They may receive: - -```text -ELITE label -stronger border treatment -higher visual emphasis -``` - -Do not create a completely separate elite hunting UI. - -## 12. World UI - -Add the Wachtposten as a real travel node. - -The world screen should communicate: - -- current location -- Verbrannte Straße connection -- Wachtposten connection -- current danger/recommended range -- future Aschengrube direction as appropriate - -## 13. Explicit non-goals - -Do not implement yet: - -```text -full quest flow -merchant -boss -area completion -Dämmerwald -``` - -## 14. Required tests - -Verify: - -- location and connection persistence -- travel works in both intended directions -- encounter pool is location-driven -- Plünderer-Veteran mechanics work -- elite encounter classification is persisted -- Danger Rating uses authoritative effective character stats -- elite loot is server-generated -- an undergeared character receives a stronger danger rating than an upgraded one where expected - -## 15. Definition of Done - -The player can experience: - -```text -reach Wachtposten -↓ -see stronger enemies -↓ -discover Elite -↓ -struggle or fail -↓ -farm upgrades -↓ -return -↓ -defeat Elite -``` - -This is the first direct playable proof of: - -> Become stronger and return. - -## 16. Handoff - -Next: - -```text -Playable Slice 0.9 – Aschengrube & First Boss -``` diff --git a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.9_Aschengrube_First_Boss.md b/docs/playable-slices/Ashen_Realms_Playable_Slice_0.9_Aschengrube_First_Boss.md deleted file mode 100644 index 807b136..0000000 --- a/docs/playable-slices/Ashen_Realms_Playable_Slice_0.9_Aschengrube_First_Boss.md +++ /dev/null @@ -1,245 +0,0 @@ -# Ashen Realms – Playable Slice 0.9: Aschengrube & First Boss - -**Status:** Ready for implementation -**Prerequisite:** Playable Slice 0.8 – Wachtposten & First Elite -**Scope:** First region climax and first boss -**Primary Location:** Aschengrube -**Boss:** Hauptmann der Aschenbande -**Next Slice:** Playable Slice 0.10 – First NPC & Quest - -## 1. Goal - -Implement the first true regional progression check. - -The player reaches the hardest Aschenfelder location, fights stronger enemies, challenges the first boss, receives guaranteed meaningful progress, and discovers the future path toward the Dämmerwald. - -## 2. New location - -Add: - -```text -Aschengrube -``` - -Connection: - -```text -Verlassener Wachtposten -↔ -Aschengrube -``` - -Use the existing world/content design for travel duration, danger, artwork, and location metadata. - -## 3. Regular encounter pool - -Core encounters: - -```text -Aschenwühler -Plünderer-Veteran -Verbrannter Jagdhund -``` - -Use the existing balancing and content definitions. - -The location should feel more dangerous than the previous two hunting areas. - -## 4. Boss access - -The boss should not be rolled as a normal random hunt encounter. - -The Aschengrube exposes a dedicated location action: - -```text -Hauptmann herausfordern -``` - -This establishes the reusable concept of a boss/location action without creating a boss-specific one-off controller. - -## 5. Boss - -Implement: - -```text -Hauptmann der Aschenbande -Level 3 -BOSS -``` - -Mechanics: - -```text -normal attack - -telegraphed Heavy Attack - -defensive phase / armor increase - -below 30% HP: -more aggressive behavior -``` - -Use reusable combat mechanics rather than a bespoke boss script framework. - -## 6. Boss duration - -Target: - -```text -8–14 rounds -``` - -This is a balancing target. - -The boss should not be realistically comfortable for a fresh character, but should become manageable through the Tier-1 progression available in the Aschenfelder. - -## 7. Boss reward rule - -Introduce the first boss guarantee system. - -### Roll A – guaranteed progression - -Always grant: - -```text -1 high-quality Tier-1 item -``` - -Guaranteed pool: - -```text -Verstärkte Lederjacke -Wachmannsbeinkleid -Aschenstiefel -Zeichen der Grenzwacht -``` - -### Roll B – additional special drops - -```text -20% Aschenklinge -8% Anhänger des verbrannten Hauptmanns -``` - -Guaranteed additional rewards: - -```text -50–70 Silver -80 XP -5 Grenzmarken -``` - -The guaranteed progression roll and special rolls are separate. - -## 8. Loot architecture extension - -Extend the existing loot system to support: - -```text -guaranteed item pool -+ -independent optional rolls -``` - -Do not implement the boss reward directly inside the controller. - -The behavior must remain data-driven and reusable. - -## 9. Boss completion persistence - -Persist first boss completion / regional progression state. - -The exact data structure should follow the existing project conventions. - -At minimum, the server must know that the character has defeated the regional boss. - -Do not rely on Angular state. - -## 10. Future path discovery - -After the first boss victory, expose the future progression hint: - -```text -Ein Pfad in Richtung Dämmerwald wurde entdeckt. -``` - -The Dämmerwald itself does not need to be playable yet. - -The world state may mark a future node/connection as discovered or unlocked according to the existing architecture. - -## 11. Boss UI - -The boss encounter should visually communicate that it is a major challenge. - -Use: - -- existing combat screen -- boss label -- stronger framing -- clear Telegraphing -- phase/status information -- existing combat log/event system - -Do not build a separate game mode. - -## 12. Explicit non-goals - -Do not implement yet: - -```text -Dämmerwald content -complex boss scripting DSL -multiple boss instances -raid mechanics -group combat -boss matchmaking -``` - -## 13. Required tests - -Verify: - -- boss can only be started through a valid server-side boss/location action -- boss combat uses persisted authoritative stats -- defensive phase changes combat behavior -- low-HP aggression triggers correctly -- boss victory persists -- boss reward is granted exactly once -- guaranteed Tier-1 item always appears -- special drop rolls remain independent -- XP, Silver and Grenzmarken match content rules -- page refresh does not duplicate boss reward -- future path discovery persists - -## 14. Definition of Done - -The player can: - -```text -reach Aschengrube -↓ -fight regular encounters -↓ -challenge boss -↓ -read and react to mechanics -↓ -win -↓ -receive guaranteed progression item -↓ -receive boss rewards -↓ -discover route toward Dämmerwald -``` - -A boss victory must never end without meaningful progress. - -## 15. Handoff - -Next: - -```text -Playable Slice 0.10 – First NPC & Quest -``` diff --git a/docs/playable-slices/README.md b/docs/playable-slices/README.md new file mode 100644 index 0000000..9298c7b --- /dev/null +++ b/docs/playable-slices/README.md @@ -0,0 +1,39 @@ +# Ashen Realms – Slice Pack 0.6.6 to 0.12 + +This pack replaces the previously planned post-0.6 progression order with a sequence aligned to the new Reputation / World Renown progression model. + +## Recommended implementation order + +1. `0.6.6-English-Game-Content-Foundation.md` +2. `0.7-Complete-Burned-Road-V2.md` +3. `0.7.5-Monster-Categories-and-Loot-Bags.md` +4. `0.8-Graufurt-Merchant-and-Trade-In.md` +5. `0.8.5-Reputation-Gated-Merchant-Offers.md` +6. `0.9-First-Quest-and-Bag-Tutorial.md` +7. `0.10-Abandoned-Watchpost.md` +8. `0.11-Ash-Pit-and-Ashen-Band-Captain.md` +9. `0.12-Ashen-Fields-Complete-and-Polish.md` + +## Core progression rule for this pack + +Normal monsters do not directly grant XP, Silver, regional reputation or World Renown. + +Instead: + +```text +Monster +→ trade goods / equipment +→ loot-bag capacity +→ merchant +→ Silver + regional reputation + World Renown +→ better bags / equipment / access +→ stronger content +``` + +Major one-time milestones such as a first area-boss victory may grant direct World Renown when explicitly configured. + +## Language rule + +From Slice 0.6.6 onward: + +> All player-facing game content is English. Development documentation may remain German. diff --git a/tools/asset-gen.mjs b/tools/asset-gen.mjs new file mode 100644 index 0000000..ad5c282 --- /dev/null +++ b/tools/asset-gen.mjs @@ -0,0 +1,180 @@ +/** + * Derives 9-slice-safe HUD assets from the hand-made art in + * `apps/web/public/assets/hud-elements/`. + * + * `panel-bg.png` carries a gem ornament at the centre of every edge. Stretched + * by `border-image` those ornaments smear, so this script paints them out + * against a plain stretch of the same edge and saves the result as a frame that + * tiles at any panel size. The ornaments are saved separately so the UI can put + * them back at the true centre of each panel. + * + * The generated files are committed, so this only needs re-running when the + * source art changes. It drives Playwright's bundled Chromium for canvas work; + * Playwright is not a direct dependency (it arrives with the Angular test + * builder), so install it explicitly if that ever stops being true. + * + * Run with the dev server up (a file:// canvas is tainted and cannot be read): + * node tools/asset-gen.mjs # defaults to http://localhost:4455 + * ASSET_ORIGIN=http://localhost:4200 node tools/asset-gen.mjs + */ +import { chromium } from 'playwright'; +import { fileURLToPath } from 'url'; +import { writeFile } from 'fs/promises'; +import path from 'path'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const outDir = path.join(root, 'apps/web/public/assets/hud-elements'); +const ORIGIN = process.env.ASSET_ORIGIN ?? 'http://localhost:4455'; + +const browser = await chromium.launch(); +const page = await browser.newPage(); +await page.goto(`${ORIGIN}/assets/hud-elements/panel-bg.png`); + +const result = await page.evaluate(async () => { + const load = async (src) => { + const img = new Image(); + img.src = src; + await img.decode(); + return img; + }; + const canvasOf = (w, h) => { + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + return c; + }; + + const img = await load('panel-bg.png'); + const src = canvasOf(img.naturalWidth, img.naturalHeight); + src.getContext('2d').drawImage(img, 0, 0); + const sctx = src.getContext('2d'); + + // ---- 1. trim the transparent margin so the frame meets the image edge ---- + const full = sctx.getImageData(0, 0, src.width, src.height).data; + const alphaAt = (x, y) => full[(y * src.width + x) * 4 + 3]; + let x0 = 0; + let x1 = src.width - 1; + let y0 = 0; + let y1 = src.height - 1; + const opaqueCol = (x) => { + for (let y = 0; y < src.height; y++) if (alphaAt(x, y) > 40) return true; + return false; + }; + const opaqueRow = (y) => { + for (let x = 0; x < src.width; x++) if (alphaAt(x, y) > 40) return true; + return false; + }; + while (x0 < x1 && !opaqueCol(x0)) x0++; + while (x1 > x0 && !opaqueCol(x1)) x1--; + while (y0 < y1 && !opaqueRow(y0)) y0++; + while (y1 > y0 && !opaqueRow(y1)) y1--; + + const W = x1 - x0 + 1; + const H = y1 - y0 + 1; + const frame = canvasOf(W, H); + const fctx = frame.getContext('2d'); + fctx.drawImage(src, x0, y0, W, H, 0, 0, W, H); + + // ---- 2. locate the centred ornament on each edge ---- + // An edge is a repeating bar away from its ornament, so a line sampled from a + // quiet stretch is a faithful stand-in. Walk out from the centre until the + // line matches that reference again: that is where the ornament ends. + const band = 96; // how deep into the panel an edge ornament reaches + const data = fctx.getImageData(0, 0, W, H).data; + const px = (x, y) => { + const i = (y * W + x) * 4; + return [data[i], data[i + 1], data[i + 2], data[i + 3]]; + }; + const diffColumns = (ax, bx, from, to) => { + let sum = 0; + for (let y = from; y < to; y++) { + const a = px(ax, y); + const b = px(bx, y); + sum += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]) + Math.abs(a[3] - b[3]); + } + return sum / (to - from); + }; + const diffRows = (ay, by, from, to) => { + let sum = 0; + for (let x = from; x < to; x++) { + const a = px(x, ay); + const b = px(x, by); + sum += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]) + Math.abs(a[3] - b[3]); + } + return sum / (to - from); + }; + + const THRESHOLD = 18; + const refX = Math.round(W * 0.26); // quiet stretch of the horizontal edges + const refY = Math.round(H * 0.26); // quiet stretch of the vertical edges + const cx = Math.round(W / 2); + const cy = Math.round(H / 2); + + const spanX = (from, to) => { + let a = cx; + let b = cx; + while (a > W * 0.15 && diffColumns(a, refX, from, to) > THRESHOLD) a--; + while (b < W * 0.85 && diffColumns(b, refX, from, to) > THRESHOLD) b++; + return [a, b]; + }; + const spanY = (from, to) => { + let a = cy; + let b = cy; + while (a > H * 0.15 && diffRows(a, refY, from, to) > THRESHOLD) a--; + while (b < H * 0.85 && diffRows(b, refY, from, to) > THRESHOLD) b++; + return [a, b]; + }; + + const [topA, topB] = spanX(0, band); + const [botA, botB] = spanX(H - band, H); + const [leftA, leftB] = spanY(0, band); + const [rightA, rightB] = spanY(W - band, W); + + // ---- 3. keep a copy of the top ornament before painting it out ---- + // The bottom edge measures the ornament most tightly (the top edge's texture + // drifts enough to over-report), so use its width for both. + const ornW = Math.max(botB - botA, 96); + const ornH = band; + const ornX = Math.round(cx - ornW / 2); + const ornament = canvasOf(ornW, ornH); + ornament.getContext('2d').drawImage(frame, ornX, 0, ornW, ornH, 0, 0, ornW, ornH); + + // ---- 4. paint each ornament out with the plain part of its own edge ---- + // `drawImage` composites, so a transparent clean column would leave an opaque + // ornament showing through. Clear the region first, then replace it. + const patchH = (a, b, y) => { + fctx.clearRect(a, y, b - a, band); + for (let x = a; x < b; x++) fctx.drawImage(frame, refX, y, 1, band, x, y, 1, band); + }; + const patchV = (a, b, x) => { + fctx.clearRect(x, a, band, b - a); + for (let y = a; y < b; y++) fctx.drawImage(frame, x, refY, band, 1, x, y, band, 1); + }; + + patchH(topA, topB, 0); + patchH(botA, botB, H - band); + patchV(leftA, leftB, 0); + patchV(rightA, rightB, W - band); + + return { + frame: frame.toDataURL('image/png'), + ornament: ornament.toDataURL('image/png'), + meta: { + source: { w: img.naturalWidth, h: img.naturalHeight }, + trimmed: { w: W, h: H }, + ornaments: { top: [topA, topB], bottom: [botA, botB], left: [leftA, leftB], right: [rightA, rightB] }, + ornamentSize: { w: ornW, h: ornH }, + }, + }; +}); + +const write = async (name, dataUrl) => { + const base64 = dataUrl.slice(dataUrl.indexOf(',') + 1); + await writeFile(path.join(outDir, name), Buffer.from(base64, 'base64')); +}; + +await write('panel-frame.png', result.frame); +await write('panel-ornament.png', result.ornament); + +console.log(JSON.stringify(result.meta, null, 2)); +await browser.close();