import { NgTemplateOutlet } from '@angular/common'; import { Component, OnInit, computed, inject } from '@angular/core'; 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'; /** * 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'; // 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: [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) { return null; } return ( this.inventoryStore.inventory()?.items.find( (item) => item.equipped && item.item.equipmentSlot === selected.item.equipmentSlot, ) ?? null ); }); ngOnInit(): void { if (this.worldStore.character() === null) { void this.worldStore.load(); } void this.inventoryStore.load(); } protected selectItem(itemId: string): void { this.inventoryStore.selectItem(itemId); } protected retry(): void { void this.inventoryStore.load(); } protected async equipSelected(characterItemId: string): Promise { await this.inventoryStore.equip(characterItemId); } }