90 lines
3.4 KiB
TypeScript
90 lines
3.4 KiB
TypeScript
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<void> {
|
|
await this.inventoryStore.equip(characterItemId);
|
|
}
|
|
}
|