inventory

This commit is contained in:
Bastian Wagner
2026-08-21 12:52:44 +02:00
parent a376fb7128
commit 987242541d
34 changed files with 3569 additions and 1814 deletions

View File

@@ -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<Record<EquipmentSlot, string>> = {
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<void> {
await this.inventoryStore.equip(characterItemId);
}
protected retry(): void {
void this.inventoryStore.load();
}
protected async equipSelected(characterItemId: string): Promise<void> {
await this.inventoryStore.equip(characterItemId);
}
}