2026-04-17 18:08:51 +08:00
|
|
|
import Phaser from 'phaser';
|
|
|
|
|
import { MutableSignal } from 'boardgame-core';
|
|
|
|
|
import {
|
|
|
|
|
type GridInventory,
|
|
|
|
|
type InventoryItem,
|
|
|
|
|
type GameItemMeta,
|
|
|
|
|
type RunState,
|
|
|
|
|
type CellKey,
|
|
|
|
|
validatePlacement,
|
|
|
|
|
removeItemFromGrid,
|
|
|
|
|
placeItem,
|
|
|
|
|
moveItem,
|
|
|
|
|
rotateItem,
|
|
|
|
|
transformShape,
|
|
|
|
|
} from 'boardgame-core/samples/slay-the-spire-like';
|
|
|
|
|
|
|
|
|
|
const ITEM_COLORS = [0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33, 0xff6633];
|
|
|
|
|
|
|
|
|
|
export interface InventoryWidgetOptions {
|
|
|
|
|
scene: Phaser.Scene;
|
|
|
|
|
gameState: MutableSignal<RunState>;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
cellSize: number;
|
|
|
|
|
gridGap?: number;
|
|
|
|
|
isLocked?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DragState {
|
|
|
|
|
itemId: string;
|
|
|
|
|
itemShape: InventoryItem<GameItemMeta>['shape'];
|
|
|
|
|
itemTransform: InventoryItem<GameItemMeta>['transform'];
|
|
|
|
|
itemMeta: InventoryItem<GameItemMeta>['meta'];
|
|
|
|
|
ghostContainer: Phaser.GameObjects.Container;
|
|
|
|
|
previewGraphics: Phaser.GameObjects.Graphics;
|
2026-04-17 19:45:34 +08:00
|
|
|
dragOffsetX: number;
|
|
|
|
|
dragOffsetY: number;
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LostItem {
|
|
|
|
|
id: string;
|
|
|
|
|
container: Phaser.GameObjects.Container;
|
2026-04-17 19:45:34 +08:00
|
|
|
shape: InventoryItem<GameItemMeta>['shape'];
|
|
|
|
|
transform: InventoryItem<GameItemMeta>['transform'];
|
|
|
|
|
meta: InventoryItem<GameItemMeta>['meta'];
|
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;
|
|
|
|
|
|
|
|
|
|
private itemContainers = new Map<string, Phaser.GameObjects.Container>();
|
|
|
|
|
private itemGraphics = new Map<string, Phaser.GameObjects.Graphics>();
|
|
|
|
|
private itemTexts = new Map<string, Phaser.GameObjects.Text>();
|
|
|
|
|
private colorMap = new Map<string, number>();
|
|
|
|
|
private colorIdx = 0;
|
|
|
|
|
|
|
|
|
|
private gridGraphics!: Phaser.GameObjects.Graphics;
|
|
|
|
|
private dragState: DragState | null = null;
|
|
|
|
|
private lostItems = new Map<string, LostItem>();
|
|
|
|
|
|
|
|
|
|
private pointerMoveHandler: (pointer: Phaser.Input.Pointer) => void;
|
|
|
|
|
private pointerUpHandler: (pointer: Phaser.Input.Pointer) => void;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
const gridW = inventory.width * this.cellSize + (inventory.width - 1) * this.gridGap;
|
|
|
|
|
const gridH = inventory.height * this.cellSize + (inventory.height - 1) * this.gridGap;
|
|
|
|
|
|
|
|
|
|
this.container = this.scene.add.container(options.x, options.y);
|
|
|
|
|
|
2026-04-17 19:45:34 +08:00
|
|
|
this.pointerMoveHandler = this.onPointerMove.bind(this);
|
|
|
|
|
this.pointerUpHandler = this.onPointerUp.bind(this);
|
|
|
|
|
|
2026-04-17 18:08:51 +08:00
|
|
|
this.drawGridBackground(inventory.width, inventory.height, gridW, gridH);
|
|
|
|
|
this.drawItems();
|
|
|
|
|
this.setupInput();
|
|
|
|
|
|
|
|
|
|
this.scene.events.once('shutdown', () => this.destroy());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getInventory(): GridInventory<GameItemMeta> {
|
|
|
|
|
return this.gameState.value.inventory as unknown as GridInventory<GameItemMeta>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private drawGridBackground(width: number, height: number, gridW: number, gridH: number): void {
|
|
|
|
|
this.gridGraphics = this.scene.add.graphics();
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
|
|
|
for (let x = 0; x < width; x++) {
|
|
|
|
|
const px = this.gridX + x * (this.cellSize + this.gridGap);
|
|
|
|
|
const py = this.gridY + y * (this.cellSize + this.gridGap);
|
|
|
|
|
|
|
|
|
|
this.gridGraphics.fillStyle(0x1a1a2e);
|
|
|
|
|
this.gridGraphics.fillRect(px, py, this.cellSize, this.cellSize);
|
|
|
|
|
this.gridGraphics.lineStyle(2, 0x444477);
|
|
|
|
|
this.gridGraphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.container.add(this.gridGraphics);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private drawItems(): void {
|
|
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
|
|
|
|
|
for (const [itemId, item] of inventory.items) {
|
|
|
|
|
if (this.itemContainers.has(itemId)) continue;
|
|
|
|
|
this.createItemVisuals(itemId, item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private createItemVisuals(itemId: string, item: InventoryItem<GameItemMeta>): void {
|
|
|
|
|
const color = this.colorMap.get(itemId) ?? ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length];
|
|
|
|
|
this.colorMap.set(itemId, color);
|
|
|
|
|
|
|
|
|
|
const graphics = this.scene.add.graphics();
|
|
|
|
|
this.itemGraphics.set(itemId, graphics);
|
|
|
|
|
|
|
|
|
|
const cells = this.getItemCells(item);
|
|
|
|
|
for (const cell of cells) {
|
|
|
|
|
const px = this.gridX + cell.x * (this.cellSize + this.gridGap);
|
|
|
|
|
const py = this.gridY + cell.y * (this.cellSize + this.gridGap);
|
|
|
|
|
|
|
|
|
|
graphics.fillStyle(color);
|
|
|
|
|
graphics.fillRect(px + 1, py + 1, this.cellSize - 2, this.cellSize - 2);
|
|
|
|
|
graphics.lineStyle(2, 0xffffff);
|
|
|
|
|
graphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const firstCell = cells[0];
|
|
|
|
|
const name = item.meta?.itemData.name ?? item.id;
|
|
|
|
|
const fontSize = Math.max(10, Math.floor(this.cellSize / 5));
|
|
|
|
|
const text = this.scene.add.text(
|
|
|
|
|
this.gridX + firstCell.x * (this.cellSize + this.gridGap) + this.cellSize / 2,
|
|
|
|
|
this.gridY + firstCell.y * (this.cellSize + this.gridGap) + this.cellSize / 2,
|
|
|
|
|
name,
|
|
|
|
|
{ fontSize: `${fontSize}px`, color: '#fff', fontStyle: 'bold' }
|
|
|
|
|
).setOrigin(0.5);
|
|
|
|
|
this.itemTexts.set(itemId, text);
|
|
|
|
|
|
|
|
|
|
const hitRect = new Phaser.Geom.Rectangle(
|
|
|
|
|
this.gridX + firstCell.x * (this.cellSize + this.gridGap),
|
|
|
|
|
this.gridY + firstCell.y * (this.cellSize + this.gridGap),
|
|
|
|
|
this.cellSize, this.cellSize
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const container = this.scene.add.container(0, 0);
|
|
|
|
|
container.add(graphics);
|
|
|
|
|
container.add(text);
|
|
|
|
|
container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains);
|
|
|
|
|
|
|
|
|
|
container.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
|
|
|
|
if (this.isLocked) return;
|
|
|
|
|
if (this.dragState) return;
|
|
|
|
|
if (pointer.button === 0) {
|
|
|
|
|
this.startDrag(itemId, pointer);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.itemContainers.set(itemId, container);
|
|
|
|
|
this.container.add(container);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getItemCells(item: InventoryItem<GameItemMeta>): { x: number; y: number }[] {
|
|
|
|
|
const cells: { x: number; y: number }[] = [];
|
|
|
|
|
const { offset } = item.transform;
|
|
|
|
|
for (let y = 0; y < item.shape.height; y++) {
|
|
|
|
|
for (let x = 0; x < item.shape.width; x++) {
|
|
|
|
|
if (item.shape.grid[y]?.[x]) {
|
|
|
|
|
cells.push({ x: x + offset.x, y: y + offset.y });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return cells;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupInput(): void {
|
|
|
|
|
this.scene.input.on('pointermove', this.pointerMoveHandler);
|
|
|
|
|
this.scene.input.on('pointerup', this.pointerUpHandler);
|
|
|
|
|
this.scene.input.on('pointerdown', this.onPointerDown.bind(this));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onPointerDown(pointer: Phaser.Input.Pointer): void {
|
|
|
|
|
if (!this.dragState) return;
|
|
|
|
|
if (pointer.button === 1) {
|
|
|
|
|
this.rotateDraggedItem();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startDrag(itemId: string, pointer: Phaser.Input.Pointer): void {
|
|
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
|
|
this.gameState.produce(state => {
|
|
|
|
|
removeItemFromGrid(state.inventory, itemId);
|
|
|
|
|
});
|
|
|
|
|
this.removeItemVisuals(itemId);
|
|
|
|
|
|
2026-04-17 19:45:34 +08:00
|
|
|
const cells = this.getItemCells(item);
|
|
|
|
|
const firstCell = cells[0];
|
|
|
|
|
const itemWorldX = this.container.x + this.gridX + firstCell.x * (this.cellSize + this.gridGap);
|
|
|
|
|
const itemWorldY = this.container.y + this.gridY + firstCell.y * (this.cellSize + this.gridGap);
|
|
|
|
|
const dragOffsetX = pointer.x - itemWorldX;
|
|
|
|
|
const dragOffsetY = pointer.y - itemWorldY;
|
|
|
|
|
|
|
|
|
|
const ghostContainer = this.scene.add.container(itemWorldX, itemWorldY).setDepth(1000);
|
2026-04-17 18:08:51 +08:00
|
|
|
const ghostGraphics = this.scene.add.graphics();
|
|
|
|
|
const color = this.colorMap.get(itemId) ?? 0x888888;
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < item.shape.height; y++) {
|
|
|
|
|
for (let x = 0; x < item.shape.width; x++) {
|
|
|
|
|
if (item.shape.grid[y]?.[x]) {
|
|
|
|
|
ghostGraphics.fillStyle(color, 0.7);
|
|
|
|
|
ghostGraphics.fillRect(x * (this.cellSize + this.gridGap), y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
|
|
|
ghostGraphics.lineStyle(2, 0xffffff);
|
|
|
|
|
ghostGraphics.strokeRect(x * (this.cellSize + this.gridGap), y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ghostContainer.add(ghostGraphics);
|
|
|
|
|
|
|
|
|
|
const previewGraphics = this.scene.add.graphics().setDepth(999).setAlpha(0.5);
|
|
|
|
|
|
|
|
|
|
this.dragState = {
|
|
|
|
|
itemId,
|
|
|
|
|
itemShape: item.shape,
|
|
|
|
|
itemTransform: { ...item.transform, offset: { ...item.transform.offset } },
|
|
|
|
|
itemMeta: item.meta,
|
|
|
|
|
ghostContainer,
|
|
|
|
|
previewGraphics,
|
2026-04-17 19:45:34 +08:00
|
|
|
dragOffsetX,
|
|
|
|
|
dragOffsetY,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startLostItemDrag(itemId: string, pointer: Phaser.Input.Pointer): void {
|
|
|
|
|
const lost = this.lostItems.get(itemId);
|
|
|
|
|
if (!lost) return;
|
|
|
|
|
|
|
|
|
|
lost.container.destroy();
|
|
|
|
|
this.lostItems.delete(itemId);
|
|
|
|
|
|
|
|
|
|
const ghostContainer = this.scene.add.container(pointer.x, pointer.y).setDepth(1000);
|
|
|
|
|
const ghostGraphics = this.scene.add.graphics();
|
|
|
|
|
const color = this.colorMap.get(itemId) ?? 0x888888;
|
|
|
|
|
|
|
|
|
|
const cells = transformShape(lost.shape, lost.transform);
|
|
|
|
|
for (const cell of cells) {
|
|
|
|
|
ghostGraphics.fillStyle(color, 0.7);
|
|
|
|
|
ghostGraphics.fillRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
|
|
|
ghostGraphics.lineStyle(2, 0xffffff);
|
|
|
|
|
ghostGraphics.strokeRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
|
|
|
}
|
|
|
|
|
ghostContainer.add(ghostGraphics);
|
|
|
|
|
|
|
|
|
|
const previewGraphics = this.scene.add.graphics().setDepth(999).setAlpha(0.5);
|
|
|
|
|
|
|
|
|
|
this.dragState = {
|
|
|
|
|
itemId,
|
|
|
|
|
itemShape: lost.shape,
|
|
|
|
|
itemTransform: { ...lost.transform, offset: { ...lost.transform.offset } },
|
|
|
|
|
itemMeta: lost.meta,
|
|
|
|
|
ghostContainer,
|
|
|
|
|
previewGraphics,
|
|
|
|
|
dragOffsetX: 0,
|
|
|
|
|
dragOffsetY: 0,
|
2026-04-17 18:08:51 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private rotateDraggedItem(): void {
|
|
|
|
|
if (!this.dragState) return;
|
|
|
|
|
|
|
|
|
|
const currentRotation = (this.dragState.itemTransform.rotation + 90) % 360;
|
|
|
|
|
this.dragState.itemTransform = {
|
|
|
|
|
...this.dragState.itemTransform,
|
|
|
|
|
rotation: currentRotation,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.updateGhostVisuals();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateGhostVisuals(): void {
|
|
|
|
|
if (!this.dragState) return;
|
|
|
|
|
|
|
|
|
|
this.dragState.ghostContainer.removeAll(true);
|
|
|
|
|
const ghostGraphics = this.scene.add.graphics();
|
|
|
|
|
const color = this.colorMap.get(this.dragState.itemId) ?? 0x888888;
|
|
|
|
|
|
|
|
|
|
const cells = transformShape(this.dragState.itemShape, this.dragState.itemTransform);
|
|
|
|
|
for (const cell of cells) {
|
|
|
|
|
ghostGraphics.fillStyle(color, 0.7);
|
|
|
|
|
ghostGraphics.fillRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
|
|
|
ghostGraphics.lineStyle(2, 0xffffff);
|
|
|
|
|
ghostGraphics.strokeRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
|
|
|
}
|
|
|
|
|
this.dragState.ghostContainer.add(ghostGraphics);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onPointerMove(pointer: Phaser.Input.Pointer): void {
|
|
|
|
|
if (!this.dragState) return;
|
|
|
|
|
|
2026-04-17 19:45:34 +08:00
|
|
|
this.dragState.ghostContainer.setPosition(pointer.x - this.dragState.dragOffsetX, pointer.y - this.dragState.dragOffsetY);
|
2026-04-17 18:08:51 +08:00
|
|
|
|
2026-04-17 19:45:34 +08:00
|
|
|
const gridCell = this.getWorldGridCell(pointer.x - this.dragState.dragOffsetX, pointer.y - this.dragState.dragOffsetY);
|
2026-04-17 18:08:51 +08:00
|
|
|
this.dragState.previewGraphics.clear();
|
|
|
|
|
|
|
|
|
|
if (gridCell) {
|
|
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
const testTransform = { ...this.dragState.itemTransform, offset: { x: gridCell.x, y: gridCell.y } };
|
|
|
|
|
const validation = validatePlacement(inventory, this.dragState.itemShape, testTransform);
|
|
|
|
|
|
|
|
|
|
const cells = transformShape(this.dragState.itemShape, testTransform);
|
|
|
|
|
for (const cell of cells) {
|
|
|
|
|
const px = this.gridX + cell.x * (this.cellSize + this.gridGap);
|
|
|
|
|
const py = this.gridY + cell.y * (this.cellSize + this.gridGap);
|
|
|
|
|
|
|
|
|
|
if (validation.valid) {
|
|
|
|
|
this.dragState.previewGraphics.fillStyle(0x33ff33, 0.3);
|
|
|
|
|
this.dragState.previewGraphics.fillRect(px, py, this.cellSize, this.cellSize);
|
|
|
|
|
this.dragState.previewGraphics.lineStyle(2, 0x33ff33);
|
|
|
|
|
this.dragState.previewGraphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
|
|
|
} else {
|
|
|
|
|
this.dragState.previewGraphics.fillStyle(0xff3333, 0.3);
|
|
|
|
|
this.dragState.previewGraphics.fillRect(px, py, this.cellSize, this.cellSize);
|
|
|
|
|
this.dragState.previewGraphics.lineStyle(2, 0xff3333);
|
|
|
|
|
this.dragState.previewGraphics.strokeRect(px, py, this.cellSize, this.cellSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onPointerUp(pointer: Phaser.Input.Pointer): void {
|
|
|
|
|
if (!this.dragState) return;
|
|
|
|
|
|
2026-04-17 19:45:34 +08:00
|
|
|
const gridCell = this.getWorldGridCell(pointer.x - this.dragState.dragOffsetX, pointer.y - this.dragState.dragOffsetY);
|
2026-04-17 18:08:51 +08:00
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
|
|
|
|
|
this.dragState.ghostContainer.destroy();
|
|
|
|
|
this.dragState.previewGraphics.destroy();
|
|
|
|
|
|
|
|
|
|
if (gridCell) {
|
|
|
|
|
const testTransform = { ...this.dragState.itemTransform, offset: { x: gridCell.x, y: gridCell.y } };
|
|
|
|
|
const validation = validatePlacement(inventory, this.dragState.itemShape, testTransform);
|
|
|
|
|
|
|
|
|
|
if (validation.valid) {
|
|
|
|
|
this.gameState.produce(state => {
|
|
|
|
|
const item: InventoryItem<GameItemMeta> = {
|
|
|
|
|
id: this.dragState!.itemId,
|
|
|
|
|
shape: this.dragState!.itemShape,
|
|
|
|
|
transform: testTransform,
|
|
|
|
|
meta: this.dragState!.itemMeta,
|
|
|
|
|
};
|
|
|
|
|
placeItem(state.inventory, item);
|
|
|
|
|
});
|
|
|
|
|
this.createItemVisualsFromDrag();
|
|
|
|
|
} else {
|
|
|
|
|
this.createLostItem();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.createLostItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.dragState = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private createItemVisualsFromDrag(): void {
|
|
|
|
|
if (!this.dragState) return;
|
|
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
const item = inventory.items.get(this.dragState.itemId);
|
|
|
|
|
if (item) {
|
|
|
|
|
this.createItemVisuals(this.dragState.itemId, item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getWorldGridCell(worldX: number, worldY: number): { x: number; y: number } | null {
|
|
|
|
|
const localX = worldX - this.container.x - this.gridX;
|
|
|
|
|
const localY = worldY - this.container.y - this.gridY;
|
|
|
|
|
|
|
|
|
|
const cellX = Math.floor(localX / (this.cellSize + this.gridGap));
|
|
|
|
|
const cellY = Math.floor(localY / (this.cellSize + this.gridGap));
|
|
|
|
|
|
2026-04-17 19:45:34 +08:00
|
|
|
if (cellX < 0 || cellY < 0 || cellX >= this.getInventory().width || cellY >= this.getInventory().height) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 18:08:51 +08:00
|
|
|
return { x: cellX, y: cellY };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private createLostItem(): void {
|
|
|
|
|
if (!this.dragState) return;
|
|
|
|
|
|
|
|
|
|
const container = this.scene.add.container(
|
|
|
|
|
this.dragState.ghostContainer.x,
|
|
|
|
|
this.dragState.ghostContainer.y
|
|
|
|
|
).setDepth(500);
|
|
|
|
|
|
|
|
|
|
const graphics = this.scene.add.graphics();
|
|
|
|
|
const color = this.colorMap.get(this.dragState.itemId) ?? 0x888888;
|
|
|
|
|
|
|
|
|
|
const cells = transformShape(this.dragState.itemShape, this.dragState.itemTransform);
|
|
|
|
|
for (const cell of cells) {
|
|
|
|
|
graphics.fillStyle(color, 0.5);
|
|
|
|
|
graphics.fillRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize - 2, this.cellSize - 2);
|
|
|
|
|
graphics.lineStyle(2, 0xff4444);
|
|
|
|
|
graphics.strokeRect(cell.x * (this.cellSize + this.gridGap), cell.y * (this.cellSize + this.gridGap), this.cellSize, this.cellSize);
|
|
|
|
|
}
|
|
|
|
|
container.add(graphics);
|
|
|
|
|
|
|
|
|
|
const name = this.dragState.itemMeta?.itemData.name ?? this.dragState.itemId;
|
|
|
|
|
const text = this.scene.add.text(0, -20, `${name} (lost)`, {
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
color: '#ff4444',
|
|
|
|
|
fontStyle: 'italic',
|
|
|
|
|
}).setOrigin(0.5);
|
|
|
|
|
container.add(text);
|
|
|
|
|
|
2026-04-17 19:45:34 +08:00
|
|
|
const hitRect = new Phaser.Geom.Rectangle(0, 0, this.cellSize, this.cellSize);
|
|
|
|
|
container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains);
|
|
|
|
|
|
|
|
|
|
container.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
|
|
|
|
if (this.isLocked) return;
|
|
|
|
|
if (this.dragState) return;
|
|
|
|
|
if (pointer.button === 0) {
|
|
|
|
|
this.startLostItemDrag(this.dragState!.itemId, pointer);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.lostItems.set(this.dragState.itemId, {
|
|
|
|
|
id: this.dragState.itemId,
|
|
|
|
|
container,
|
|
|
|
|
shape: this.dragState.itemShape,
|
|
|
|
|
transform: { ...this.dragState.itemTransform },
|
|
|
|
|
meta: this.dragState.itemMeta,
|
|
|
|
|
});
|
2026-04-17 18:08:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private removeItemVisuals(itemId: string): void {
|
|
|
|
|
this.itemContainers.get(itemId)?.destroy();
|
|
|
|
|
this.itemGraphics.get(itemId)?.destroy();
|
|
|
|
|
this.itemTexts.get(itemId)?.destroy();
|
|
|
|
|
this.itemContainers.delete(itemId);
|
|
|
|
|
this.itemGraphics.delete(itemId);
|
|
|
|
|
this.itemTexts.delete(itemId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setLocked(locked: boolean): void {
|
|
|
|
|
this.isLocked = locked;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getLostItems(): string[] {
|
|
|
|
|
return Array.from(this.lostItems.keys());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public clearLostItems(): void {
|
|
|
|
|
for (const lost of this.lostItems.values()) {
|
|
|
|
|
lost.container.destroy();
|
|
|
|
|
}
|
|
|
|
|
this.lostItems.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public refresh(): void {
|
|
|
|
|
const inventory = this.getInventory();
|
|
|
|
|
|
|
|
|
|
for (const itemId of this.itemContainers.keys()) {
|
|
|
|
|
if (!inventory.items.has(itemId)) {
|
|
|
|
|
this.removeItemVisuals(itemId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [itemId, item] of inventory.items) {
|
|
|
|
|
if (!this.itemContainers.has(itemId)) {
|
|
|
|
|
this.createItemVisuals(itemId, item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public destroy(): void {
|
|
|
|
|
this.scene.input.off('pointermove', this.pointerMoveHandler);
|
|
|
|
|
this.scene.input.off('pointerup', this.pointerUpHandler);
|
|
|
|
|
|
|
|
|
|
if (this.dragState) {
|
|
|
|
|
this.dragState.ghostContainer.destroy();
|
|
|
|
|
this.dragState.previewGraphics.destroy();
|
|
|
|
|
this.dragState = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.clearLostItems();
|
|
|
|
|
|
|
|
|
|
for (const container of this.itemContainers.values()) {
|
|
|
|
|
container.destroy();
|
|
|
|
|
}
|
|
|
|
|
this.itemContainers.clear();
|
|
|
|
|
this.itemGraphics.clear();
|
|
|
|
|
this.itemTexts.clear();
|
|
|
|
|
|
|
|
|
|
this.gridGraphics.destroy();
|
|
|
|
|
this.container.destroy();
|
|
|
|
|
}
|
|
|
|
|
}
|