2026-04-19 00:01:25 +08:00
|
|
|
import Phaser from "phaser";
|
|
|
|
|
import { MutableSignal } from "boardgame-core";
|
2026-04-17 18:08:51 +08:00
|
|
|
import {
|
|
|
|
|
type GridInventory,
|
|
|
|
|
type InventoryItem,
|
|
|
|
|
type GameItemMeta,
|
|
|
|
|
type RunState,
|
|
|
|
|
removeItemFromGrid,
|
|
|
|
|
placeItem,
|
2026-04-19 00:01:25 +08:00
|
|
|
} from "boardgame-core/samples/slay-the-spire-like";
|
|
|
|
|
import { ItemRenderer } from "./ItemRenderer";
|
|
|
|
|
import { DragController } from "./DragController";
|
|
|
|
|
import { LostItemManager } from "./LostItemManager";
|
2026-04-17 18:08:51 +08:00
|
|
|
|
|
|
|
|
export interface InventoryWidgetOptions {
|
|
|
|
|
scene: Phaser.Scene;
|
|
|
|
|
gameState: MutableSignal<RunState>;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
cellSize: number;
|
|
|
|
|
gridGap?: number;
|
|
|
|
|
isLocked?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
/**
|
|
|
|
|
* Thin orchestrator for the inventory grid widget.
|
|
|
|
|
* Delegates rendering, drag logic, and lost-item management to focused modules.
|
|
|
|
|
*/
|
2026-04-17 18:08:51 +08:00
|
|
|
export class InventoryWidget {
|
|
|
|
|
private scene: Phaser.Scene;
|
|
|
|
|
private gameState: MutableSignal<RunState>;
|
|
|
|
|
private container: Phaser.GameObjects.Container;
|
|
|
|
|
private cellSize: number;
|
|
|
|
|
private gridGap: number;
|
|
|
|
|
private gridX = 0;
|
|
|
|
|
private gridY = 0;
|
|
|
|
|
private isLocked: boolean;
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private renderer: ItemRenderer;
|
|
|
|
|
private dragController: DragController;
|
|
|
|
|
private lostItemManager: LostItemManager;
|
2026-04-17 18:08:51 +08:00
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private pointerMoveHandler!: (pointer: Phaser.Input.Pointer) => void;
|
|
|
|
|
private pointerUpHandler!: (pointer: Phaser.Input.Pointer) => void;
|
|
|
|
|
private pointerDownHandler!: (pointer: Phaser.Input.Pointer) => void;
|
2026-04-17 18:08:51 +08:00
|
|
|
|
|
|
|
|
constructor(options: InventoryWidgetOptions) {
|
|
|
|
|
this.scene = options.scene;
|
|
|
|
|
this.gameState = options.gameState;
|
|
|
|
|
this.cellSize = options.cellSize;
|
|
|
|
|
this.gridGap = options.gridGap ?? 2;
|
|
|
|
|
this.isLocked = options.isLocked ?? false;
|
|
|
|
|
|
|
|
|
|
const inventory = this.gameState.value.inventory;
|
2026-04-19 00:01:25 +08:00
|
|
|
const gridW =
|
|
|
|
|
inventory.width * this.cellSize + (inventory.width - 1) * this.gridGap;
|
|
|
|
|
const gridH =
|
|
|
|
|
inventory.height * this.cellSize + (inventory.height - 1) * this.gridGap;
|
2026-04-17 18:08:51 +08:00
|
|
|
|
|
|
|
|
this.container = this.scene.add.container(options.x, options.y);
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
// Initialize sub-modules
|
|
|
|
|
this.renderer = new ItemRenderer({
|
|
|
|
|
scene: this.scene,
|
|
|
|
|
container: this.container,
|
|
|
|
|
cellSize: this.cellSize,
|
|
|
|
|
gridGap: this.gridGap,
|
|
|
|
|
gridX: this.gridX,
|
|
|
|
|
gridY: this.gridY,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.dragController = new DragController({
|
|
|
|
|
scene: this.scene,
|
|
|
|
|
container: this.container,
|
|
|
|
|
cellSize: this.cellSize,
|
|
|
|
|
gridGap: this.gridGap,
|
|
|
|
|
gridX: this.gridX,
|
|
|
|
|
gridY: this.gridY,
|
|
|
|
|
getInventory: () => this.getInventory(),
|
|
|
|
|
getItemColor: (id) => this.renderer.getItemColor(id),
|
|
|
|
|
getItemCells: (item) => this.renderer.getItemCells(item),
|
|
|
|
|
onPlaceItem: (item) => this.handlePlaceItem(item),
|
|
|
|
|
onCreateLostItem: (id, shape, transform, meta) =>
|
|
|
|
|
this.handleCreateLostItem(id, shape, transform, meta),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.lostItemManager = new LostItemManager({
|
|
|
|
|
scene: this.scene,
|
|
|
|
|
cellSize: this.cellSize,
|
|
|
|
|
gridGap: this.gridGap,
|
|
|
|
|
getItemColor: (id) => this.renderer.getItemColor(id),
|
|
|
|
|
onLostItemDragStart: (id, pointer) =>
|
|
|
|
|
this.dragController.startLostItemDrag(
|
|
|
|
|
id,
|
|
|
|
|
this.getLostItemShape(id),
|
|
|
|
|
this.getLostItemTransform(id),
|
|
|
|
|
this.getLostItemMeta(id),
|
|
|
|
|
pointer,
|
|
|
|
|
),
|
|
|
|
|
isDragging: () => this.dragController.isDragging(),
|
|
|
|
|
});
|
2026-04-17 19:45:34 +08:00
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
this.renderer.drawGridBackground(inventory.width, inventory.height);
|
2026-04-17 18:08:51 +08:00
|
|
|
this.drawItems();
|
|
|
|
|
this.setupInput();
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
this.scene.events.once("shutdown", () => this.destroy());
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getInventory(): GridInventory<GameItemMeta> {
|
2026-04-19 00:01:25 +08:00
|
|
|
return this.gameState.value
|
|
|
|
|
.inventory as unknown as GridInventory<GameItemMeta>;
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private drawItems(): void {
|
|
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
for (const [itemId, item] of inventory.items) {
|
2026-04-19 00:01:25 +08:00
|
|
|
if (this.renderer.hasItem(itemId)) continue;
|
|
|
|
|
const visuals = this.renderer.createItemVisuals(itemId, item);
|
|
|
|
|
this.setupItemInteraction(itemId, visuals);
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private setupItemInteraction(
|
|
|
|
|
itemId: string,
|
|
|
|
|
visuals: ReturnType<typeof ItemRenderer.prototype.createItemVisuals>,
|
|
|
|
|
): void {
|
|
|
|
|
visuals.container.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
2026-04-17 18:08:51 +08:00
|
|
|
if (this.isLocked) return;
|
2026-04-19 00:01:25 +08:00
|
|
|
if (this.dragController.isDragging()) return;
|
2026-04-17 18:08:51 +08:00
|
|
|
if (pointer.button === 0) {
|
2026-04-19 00:01:25 +08:00
|
|
|
this.gameState.produce((state) => {
|
|
|
|
|
removeItemFromGrid(state.inventory, itemId);
|
|
|
|
|
});
|
|
|
|
|
this.renderer.removeItemVisuals(itemId);
|
|
|
|
|
this.dragController.startDrag(itemId, pointer);
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupInput(): void {
|
2026-04-19 00:01:25 +08:00
|
|
|
this.pointerDownHandler = (pointer: Phaser.Input.Pointer) => {
|
|
|
|
|
if (!this.dragController.isDragging()) return;
|
|
|
|
|
if (pointer.button === 1) {
|
|
|
|
|
this.dragController.rotateDraggedItem();
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
2026-04-17 19:45:34 +08:00
|
|
|
};
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
this.pointerMoveHandler = (pointer: Phaser.Input.Pointer) => {
|
|
|
|
|
this.dragController.onPointerMove(pointer);
|
2026-04-17 18:08:51 +08:00
|
|
|
};
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
this.pointerUpHandler = (pointer: Phaser.Input.Pointer) => {
|
|
|
|
|
this.dragController.onPointerUp(pointer);
|
2026-04-17 18:08:51 +08:00
|
|
|
};
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
this.scene.input.on("pointermove", this.pointerMoveHandler);
|
|
|
|
|
this.scene.input.on("pointerup", this.pointerUpHandler);
|
|
|
|
|
this.scene.input.on("pointerdown", this.pointerDownHandler);
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private handlePlaceItem(item: InventoryItem<GameItemMeta>): void {
|
|
|
|
|
this.gameState.produce((state) => {
|
|
|
|
|
placeItem(state.inventory, item);
|
|
|
|
|
});
|
2026-04-17 18:08:51 +08:00
|
|
|
const inventory = this.getInventory();
|
2026-04-19 00:01:25 +08:00
|
|
|
const placedItem = inventory.items.get(item.id);
|
|
|
|
|
if (placedItem) {
|
|
|
|
|
const visuals = this.renderer.createItemVisuals(item.id, placedItem);
|
|
|
|
|
this.setupItemInteraction(item.id, visuals);
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private handleCreateLostItem(
|
|
|
|
|
itemId: string,
|
|
|
|
|
shape: InventoryItem<GameItemMeta>["shape"],
|
|
|
|
|
transform: InventoryItem<GameItemMeta>["transform"],
|
|
|
|
|
meta: InventoryItem<GameItemMeta>["meta"],
|
|
|
|
|
): void {
|
|
|
|
|
this.lostItemManager.createLostItem(
|
|
|
|
|
itemId,
|
|
|
|
|
shape,
|
|
|
|
|
transform,
|
|
|
|
|
meta,
|
|
|
|
|
this.dragController.getDraggedItemPosition().x,
|
|
|
|
|
this.dragController.getDraggedItemPosition().y,
|
|
|
|
|
);
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private getLostItemShape(itemId: string) {
|
|
|
|
|
return this.lostItemManager.getLostItem(itemId)?.shape!;
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private getLostItemTransform(itemId: string) {
|
|
|
|
|
return this.lostItemManager.getLostItem(itemId)?.transform!;
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
private getLostItemMeta(itemId: string) {
|
|
|
|
|
return this.lostItemManager.getLostItem(itemId)?.meta!;
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setLocked(locked: boolean): void {
|
|
|
|
|
this.isLocked = locked;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getLostItems(): string[] {
|
2026-04-19 00:01:25 +08:00
|
|
|
return this.lostItemManager.getLostItemIds();
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public clearLostItems(): void {
|
2026-04-19 00:01:25 +08:00
|
|
|
this.lostItemManager.clear();
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public refresh(): void {
|
|
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
// Remove visuals for items no longer in inventory
|
|
|
|
|
for (const [itemId] of inventory.items.entries()) {
|
|
|
|
|
// We need a way to track which items have visuals
|
|
|
|
|
// For now, clear and redraw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simple approach: destroy all and redraw
|
|
|
|
|
this.renderer.destroy();
|
|
|
|
|
this.renderer = new ItemRenderer({
|
|
|
|
|
scene: this.scene,
|
|
|
|
|
container: this.container,
|
|
|
|
|
cellSize: this.cellSize,
|
|
|
|
|
gridGap: this.gridGap,
|
|
|
|
|
gridX: this.gridX,
|
|
|
|
|
gridY: this.gridY,
|
|
|
|
|
});
|
|
|
|
|
this.renderer.drawGridBackground(inventory.width, inventory.height);
|
|
|
|
|
this.drawItems();
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public destroy(): void {
|
2026-04-19 00:01:25 +08:00
|
|
|
this.scene.input.off("pointermove", this.pointerMoveHandler);
|
|
|
|
|
this.scene.input.off("pointerup", this.pointerUpHandler);
|
|
|
|
|
this.scene.input.off("pointerdown", this.pointerDownHandler);
|
2026-04-17 18:08:51 +08:00
|
|
|
|
2026-04-19 00:01:25 +08:00
|
|
|
this.dragController.destroy();
|
|
|
|
|
this.lostItemManager.destroy();
|
|
|
|
|
this.renderer.destroy();
|
2026-04-17 18:08:51 +08:00
|
|
|
this.container.destroy();
|
|
|
|
|
}
|
|
|
|
|
}
|