refactor: remove inventory widget and drag controller
Removes the inventory management system and drag-and-drop functionality from the STS-like viewer. This includes deleting the `InventoryWidget`, `InventoryItemSpawner`, and `DragController` classes, as well as cleaning up references in `PlaceholderEncounterScene`.
This commit is contained in:
parent
82df3f2a2f
commit
34a7cff964
|
|
@ -1,26 +1,22 @@
|
||||||
import Phaser from "phaser";
|
|
||||||
import { ReactiveScene } from "boardgame-phaser";
|
|
||||||
import { createButton } from "@/utils/createButton";
|
|
||||||
import { UI_CONFIG, GRID_CONFIG, NODE_COLORS, NODE_LABELS } from "@/config";
|
|
||||||
import { MutableSignal } from "boardgame-core";
|
|
||||||
import {
|
import {
|
||||||
resolveEncounter,
|
resolveEncounter,
|
||||||
removeItem,
|
|
||||||
type RunState,
|
type RunState,
|
||||||
type EncounterResult,
|
type EncounterResult,
|
||||||
type MapNodeType,
|
type MapNodeType,
|
||||||
type MapNode,
|
type MapNode,
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
import { InventoryWidget } from "@/widgets/InventoryWidget";
|
import { ReactiveScene } from "boardgame-phaser";
|
||||||
|
|
||||||
|
import type { MutableSignal } from "boardgame-core";
|
||||||
|
|
||||||
|
import { UI_CONFIG, GRID_CONFIG, NODE_COLORS, NODE_LABELS } from "@/config";
|
||||||
|
import { createButton } from "@/utils/createButton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 占位符遭遇场景
|
* 占位符遭遇场景
|
||||||
*
|
|
||||||
* 左侧显示背包网格(使用 InventoryWidget),右侧显示遭遇信息。
|
|
||||||
*/
|
*/
|
||||||
export class PlaceholderEncounterScene extends ReactiveScene {
|
export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
private gameState: MutableSignal<RunState>;
|
private gameState: MutableSignal<RunState>;
|
||||||
private inventoryWidget: InventoryWidget | null = null;
|
|
||||||
|
|
||||||
constructor(gameState: MutableSignal<RunState>) {
|
constructor(gameState: MutableSignal<RunState>) {
|
||||||
super("PlaceholderEncounterScene");
|
super("PlaceholderEncounterScene");
|
||||||
|
|
@ -39,30 +35,9 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
const gridH = gridRows * cellSize + (gridRows - 1) * GRID_CONFIG.GRID_GAP;
|
const gridH = gridRows * cellSize + (gridRows - 1) * GRID_CONFIG.GRID_GAP;
|
||||||
const leftPanelW = gridW + 40;
|
const leftPanelW = gridW + 40;
|
||||||
|
|
||||||
this.inventoryWidget = new InventoryWidget({
|
|
||||||
scene: this,
|
|
||||||
gameState: this.gameState,
|
|
||||||
x: 60,
|
|
||||||
y: (height - gridH) / 2 + 20,
|
|
||||||
cellSize,
|
|
||||||
gridGap: GRID_CONFIG.GRID_GAP,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.cameras.main.setBounds(0, 0, width, height);
|
this.cameras.main.setBounds(0, 0, width, height);
|
||||||
this.cameras.main.setScroll(0, 0);
|
this.cameras.main.setScroll(0, 0);
|
||||||
|
|
||||||
// Panel background
|
|
||||||
this.add
|
|
||||||
.rectangle(
|
|
||||||
60 + leftPanelW / 2,
|
|
||||||
this.inventoryWidgetY(gridH),
|
|
||||||
leftPanelW + 10,
|
|
||||||
gridH + 50,
|
|
||||||
0x111122,
|
|
||||||
0.9,
|
|
||||||
)
|
|
||||||
.setStrokeStyle(2, 0x5555aa);
|
|
||||||
|
|
||||||
// "背包" title
|
// "背包" title
|
||||||
this.add
|
this.add
|
||||||
.text(60 + gridW / 2, (height - gridH) / 2, "背包", {
|
.text(60 + gridW / 2, (height - gridH) / 2, "背包", {
|
||||||
|
|
@ -92,11 +67,6 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private inventoryWidgetY(gridH: number): number {
|
|
||||||
const { height } = this.scale;
|
|
||||||
return (height - gridH) / 2 + 20 + gridH / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawRightPanel(
|
private drawRightPanel(
|
||||||
node: MapNode & { encounter: { name: string; description: string } },
|
node: MapNode & { encounter: { name: string; description: string } },
|
||||||
leftPanelW: number,
|
leftPanelW: number,
|
||||||
|
|
@ -201,15 +171,6 @@ export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
const node = state.map.nodes.get(state.currentNodeId);
|
const node = state.map.nodes.get(state.currentNodeId);
|
||||||
if (!node || !node.encounter) return;
|
if (!node || !node.encounter) return;
|
||||||
|
|
||||||
// Clear lost items from inventory
|
|
||||||
if (this.inventoryWidget) {
|
|
||||||
const lostIds = this.inventoryWidget.getLostItems();
|
|
||||||
for (const lostId of lostIds) {
|
|
||||||
removeItem(state, lostId);
|
|
||||||
}
|
|
||||||
this.inventoryWidget.clearLostItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: EncounterResult = this.generatePlaceholderResult(node.type);
|
const result: EncounterResult = this.generatePlaceholderResult(node.type);
|
||||||
resolveEncounter(state, result);
|
resolveEncounter(state, result);
|
||||||
await this.sceneController.launch("GameFlowScene");
|
await this.sceneController.launch("GameFlowScene");
|
||||||
|
|
|
||||||
|
|
@ -1,477 +0,0 @@
|
||||||
import Phaser from "phaser";
|
|
||||||
import {
|
|
||||||
type InventoryItem,
|
|
||||||
type GameItemMeta,
|
|
||||||
type GridInventory,
|
|
||||||
validatePlacement,
|
|
||||||
transformShape,
|
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
|
||||||
import { dragDropEventEffect, DragDropEventType } from "boardgame-phaser";
|
|
||||||
import { DisposableBag } from "boardgame-phaser";
|
|
||||||
|
|
||||||
export interface DragSession {
|
|
||||||
itemId: string;
|
|
||||||
itemShape: InventoryItem<GameItemMeta>["shape"];
|
|
||||||
itemTransform: InventoryItem<GameItemMeta>["transform"];
|
|
||||||
itemMeta: InventoryItem<GameItemMeta>["meta"];
|
|
||||||
ghostContainer: Phaser.GameObjects.Container;
|
|
||||||
previewGraphics: Phaser.GameObjects.Graphics;
|
|
||||||
disposables: DisposableBag;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DragControllerOptions {
|
|
||||||
scene: Phaser.Scene;
|
|
||||||
container: Phaser.GameObjects.Container;
|
|
||||||
cellSize: number;
|
|
||||||
gridGap: number;
|
|
||||||
gridX: number;
|
|
||||||
gridY: number;
|
|
||||||
getInventory: () => GridInventory<GameItemMeta>;
|
|
||||||
getItemColor: (itemId: string) => number;
|
|
||||||
onPlaceItem: (item: InventoryItem<GameItemMeta>) => void;
|
|
||||||
onCreateLostItem: (
|
|
||||||
itemId: string,
|
|
||||||
shape: InventoryItem<GameItemMeta>["shape"],
|
|
||||||
transform: InventoryItem<GameItemMeta>["transform"],
|
|
||||||
meta: InventoryItem<GameItemMeta>["meta"],
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event-driven drag controller using dragDropEventEffect from boardgame-phaser.
|
|
||||||
* Manages ghost visuals, placement preview, rotation, and validation.
|
|
||||||
*/
|
|
||||||
export class DragController {
|
|
||||||
private scene: Phaser.Scene;
|
|
||||||
private container: Phaser.GameObjects.Container;
|
|
||||||
private cellSize: number;
|
|
||||||
private gridGap: number;
|
|
||||||
private gridX: number;
|
|
||||||
private gridY: number;
|
|
||||||
private getInventory: () => GridInventory<GameItemMeta>;
|
|
||||||
private getItemColor: (itemId: string) => number;
|
|
||||||
private onPlaceItem: (item: InventoryItem<GameItemMeta>) => void;
|
|
||||||
private onCreateLostItem: (
|
|
||||||
itemId: string,
|
|
||||||
shape: InventoryItem<GameItemMeta>["shape"],
|
|
||||||
transform: InventoryItem<GameItemMeta>["transform"],
|
|
||||||
meta: InventoryItem<GameItemMeta>["meta"],
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
private activeSession: DragSession | null = null;
|
|
||||||
|
|
||||||
constructor(options: DragControllerOptions) {
|
|
||||||
this.scene = options.scene;
|
|
||||||
this.container = options.container;
|
|
||||||
this.cellSize = options.cellSize;
|
|
||||||
this.gridGap = options.gridGap;
|
|
||||||
this.gridX = options.gridX;
|
|
||||||
this.gridY = options.gridY;
|
|
||||||
this.getInventory = options.getInventory;
|
|
||||||
this.getItemColor = options.getItemColor;
|
|
||||||
this.onPlaceItem = options.onPlaceItem;
|
|
||||||
this.onCreateLostItem = options.onCreateLostItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a drag session for an inventory item.
|
|
||||||
* Uses dragDropEventEffect for pointer tracking and event emission.
|
|
||||||
*/
|
|
||||||
startDrag(
|
|
||||||
itemId: string,
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
itemContainer: Phaser.GameObjects.Container,
|
|
||||||
): () => void {
|
|
||||||
const cells = this.getItemCells(item);
|
|
||||||
const firstCell = cells[0];
|
|
||||||
const worldX =
|
|
||||||
this.container.x +
|
|
||||||
this.gridX +
|
|
||||||
firstCell.x * (this.cellSize + this.gridGap);
|
|
||||||
const worldY =
|
|
||||||
this.container.y +
|
|
||||||
this.gridY +
|
|
||||||
firstCell.y * (this.cellSize + this.gridGap);
|
|
||||||
|
|
||||||
const ghostContainer = this.createGhostContainer(
|
|
||||||
worldX,
|
|
||||||
worldY,
|
|
||||||
item.shape,
|
|
||||||
item.transform,
|
|
||||||
this.getItemColor(itemId),
|
|
||||||
);
|
|
||||||
const previewGraphics = this.scene.add
|
|
||||||
.graphics()
|
|
||||||
.setDepth(999)
|
|
||||||
.setAlpha(0.5);
|
|
||||||
|
|
||||||
const disposables = new DisposableBag();
|
|
||||||
const session: DragSession = {
|
|
||||||
itemId,
|
|
||||||
itemShape: item.shape,
|
|
||||||
itemTransform: {
|
|
||||||
...item.transform,
|
|
||||||
offset: { ...item.transform.offset },
|
|
||||||
},
|
|
||||||
itemMeta: item.meta,
|
|
||||||
ghostContainer,
|
|
||||||
previewGraphics,
|
|
||||||
disposables,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.activeSession = session;
|
|
||||||
|
|
||||||
// Set up drag-drop event handling via framework utility
|
|
||||||
const disposeDrag = dragDropEventEffect(
|
|
||||||
itemContainer as Phaser.GameObjects.GameObject,
|
|
||||||
disposables,
|
|
||||||
);
|
|
||||||
|
|
||||||
itemContainer.on("dragstart", () => {
|
|
||||||
ghostContainer.setVisible(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
itemContainer.on("dragmove", () => {
|
|
||||||
this.handleDragMove(session);
|
|
||||||
});
|
|
||||||
|
|
||||||
itemContainer.on("dragend", () => {
|
|
||||||
this.handleDragEnd(session);
|
|
||||||
disposeDrag();
|
|
||||||
this.activeSession = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disposeDrag();
|
|
||||||
this.destroySession(session);
|
|
||||||
this.activeSession = null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a drag session for a lost item.
|
|
||||||
*/
|
|
||||||
startLostItemDrag(
|
|
||||||
itemId: string,
|
|
||||||
shape: InventoryItem<GameItemMeta>["shape"],
|
|
||||||
transform: InventoryItem<GameItemMeta>["transform"],
|
|
||||||
meta: InventoryItem<GameItemMeta>["meta"],
|
|
||||||
lostContainer: Phaser.GameObjects.Container,
|
|
||||||
): () => void {
|
|
||||||
const pointer = this.scene.input.activePointer;
|
|
||||||
const ghostContainer = this.createGhostContainer(
|
|
||||||
pointer.x,
|
|
||||||
pointer.y,
|
|
||||||
shape,
|
|
||||||
transform,
|
|
||||||
this.getItemColor(itemId),
|
|
||||||
);
|
|
||||||
const previewGraphics = this.scene.add
|
|
||||||
.graphics()
|
|
||||||
.setDepth(999)
|
|
||||||
.setAlpha(0.5);
|
|
||||||
|
|
||||||
const disposables = new DisposableBag();
|
|
||||||
const session: DragSession = {
|
|
||||||
itemId,
|
|
||||||
itemShape: shape,
|
|
||||||
itemTransform: { ...transform, offset: { ...transform.offset } },
|
|
||||||
itemMeta: meta,
|
|
||||||
ghostContainer,
|
|
||||||
previewGraphics,
|
|
||||||
disposables,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.activeSession = session;
|
|
||||||
|
|
||||||
const disposeDrag = dragDropEventEffect(
|
|
||||||
lostContainer as Phaser.GameObjects.GameObject,
|
|
||||||
disposables,
|
|
||||||
);
|
|
||||||
|
|
||||||
lostContainer.on("dragstart", () => {
|
|
||||||
ghostContainer.setVisible(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
lostContainer.on("dragmove", () => {
|
|
||||||
this.handleDragMove(session);
|
|
||||||
});
|
|
||||||
|
|
||||||
lostContainer.on("dragend", () => {
|
|
||||||
this.handleDragEnd(session);
|
|
||||||
disposeDrag();
|
|
||||||
this.activeSession = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disposeDrag();
|
|
||||||
this.destroySession(session);
|
|
||||||
this.activeSession = null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotate the currently dragged item by 90 degrees.
|
|
||||||
*/
|
|
||||||
rotateDraggedItem(): void {
|
|
||||||
if (!this.activeSession) return;
|
|
||||||
|
|
||||||
const currentRotation =
|
|
||||||
(this.activeSession.itemTransform.rotation + 90) % 360;
|
|
||||||
this.activeSession.itemTransform = {
|
|
||||||
...this.activeSession.itemTransform,
|
|
||||||
rotation: currentRotation,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateGhostVisuals(this.activeSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if currently dragging.
|
|
||||||
*/
|
|
||||||
isDragging(): boolean {
|
|
||||||
return this.activeSession !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of the item being dragged, or null.
|
|
||||||
*/
|
|
||||||
getDraggedItemId(): string | null {
|
|
||||||
return this.activeSession?.itemId ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current position of the dragged ghost container.
|
|
||||||
*/
|
|
||||||
getDraggedItemPosition(): { x: number; y: number } {
|
|
||||||
if (!this.activeSession) return { x: 0, y: 0 };
|
|
||||||
return {
|
|
||||||
x: this.activeSession.ghostContainer.x,
|
|
||||||
y: this.activeSession.ghostContainer.y,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up active session and destroy all visuals.
|
|
||||||
*/
|
|
||||||
destroy(): void {
|
|
||||||
if (this.activeSession) {
|
|
||||||
this.destroySession(this.activeSession);
|
|
||||||
this.activeSession = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createGhostContainer(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
shape: InventoryItem<GameItemMeta>["shape"],
|
|
||||||
transform: InventoryItem<GameItemMeta>["transform"],
|
|
||||||
color: number,
|
|
||||||
): Phaser.GameObjects.Container {
|
|
||||||
const ghostContainer = this.scene.add.container(x, y).setDepth(1000);
|
|
||||||
const ghostGraphics = this.scene.add.graphics();
|
|
||||||
|
|
||||||
const cells = transformShape(shape, 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);
|
|
||||||
|
|
||||||
return ghostContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateGhostVisuals(session: DragSession): void {
|
|
||||||
session.ghostContainer.removeAll(true);
|
|
||||||
const ghostGraphics = this.scene.add.graphics();
|
|
||||||
const color = this.getItemColor(session.itemId);
|
|
||||||
|
|
||||||
const cells = transformShape(session.itemShape, session.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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
session.ghostContainer.add(ghostGraphics);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDragMove(session: DragSession): void {
|
|
||||||
const pointer = this.scene.input.activePointer;
|
|
||||||
session.ghostContainer.setPosition(pointer.x, pointer.y);
|
|
||||||
|
|
||||||
const gridCell = this.getWorldGridCell(pointer.x, pointer.y);
|
|
||||||
session.previewGraphics.clear();
|
|
||||||
|
|
||||||
if (gridCell) {
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
const testTransform = {
|
|
||||||
...session.itemTransform,
|
|
||||||
offset: { x: gridCell.x, y: gridCell.y },
|
|
||||||
};
|
|
||||||
const validation = validatePlacement(
|
|
||||||
inventory,
|
|
||||||
session.itemShape,
|
|
||||||
testTransform,
|
|
||||||
);
|
|
||||||
|
|
||||||
const cells = transformShape(session.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) {
|
|
||||||
session.previewGraphics.fillStyle(0x33ff33, 0.3);
|
|
||||||
session.previewGraphics.fillRect(
|
|
||||||
px,
|
|
||||||
py,
|
|
||||||
this.cellSize,
|
|
||||||
this.cellSize,
|
|
||||||
);
|
|
||||||
session.previewGraphics.lineStyle(2, 0x33ff33);
|
|
||||||
session.previewGraphics.strokeRect(
|
|
||||||
px,
|
|
||||||
py,
|
|
||||||
this.cellSize,
|
|
||||||
this.cellSize,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
session.previewGraphics.fillStyle(0xff3333, 0.3);
|
|
||||||
session.previewGraphics.fillRect(
|
|
||||||
px,
|
|
||||||
py,
|
|
||||||
this.cellSize,
|
|
||||||
this.cellSize,
|
|
||||||
);
|
|
||||||
session.previewGraphics.lineStyle(2, 0xff3333);
|
|
||||||
session.previewGraphics.strokeRect(
|
|
||||||
px,
|
|
||||||
py,
|
|
||||||
this.cellSize,
|
|
||||||
this.cellSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDragEnd(session: DragSession): void {
|
|
||||||
const pointer = this.scene.input.activePointer;
|
|
||||||
const gridCell = this.getWorldGridCell(pointer.x, pointer.y);
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
|
|
||||||
session.ghostContainer.destroy();
|
|
||||||
session.previewGraphics.destroy();
|
|
||||||
session.disposables.dispose();
|
|
||||||
|
|
||||||
if (gridCell) {
|
|
||||||
const testTransform = {
|
|
||||||
...session.itemTransform,
|
|
||||||
offset: { x: gridCell.x, y: gridCell.y },
|
|
||||||
};
|
|
||||||
const validation = validatePlacement(
|
|
||||||
inventory,
|
|
||||||
session.itemShape,
|
|
||||||
testTransform,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
const item: InventoryItem<GameItemMeta> = {
|
|
||||||
id: session.itemId,
|
|
||||||
shape: session.itemShape,
|
|
||||||
transform: testTransform,
|
|
||||||
meta: session.itemMeta,
|
|
||||||
};
|
|
||||||
this.onPlaceItem(item);
|
|
||||||
} else {
|
|
||||||
this.onCreateLostItem(
|
|
||||||
session.itemId,
|
|
||||||
session.itemShape,
|
|
||||||
session.itemTransform,
|
|
||||||
session.itemMeta,
|
|
||||||
session.ghostContainer.x,
|
|
||||||
session.ghostContainer.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.onCreateLostItem(
|
|
||||||
session.itemId,
|
|
||||||
session.itemShape,
|
|
||||||
session.itemTransform,
|
|
||||||
session.itemMeta,
|
|
||||||
session.ghostContainer.x,
|
|
||||||
session.ghostContainer.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
if (
|
|
||||||
cellX < 0 ||
|
|
||||||
cellY < 0 ||
|
|
||||||
cellX >= inventory.width ||
|
|
||||||
cellY >= inventory.height
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { x: cellX, y: cellY };
|
|
||||||
}
|
|
||||||
|
|
||||||
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 destroySession(session: DragSession): void {
|
|
||||||
session.disposables.dispose();
|
|
||||||
session.ghostContainer.destroy();
|
|
||||||
session.previewGraphics.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
import Phaser from "phaser";
|
|
||||||
import { MutableSignal } from "boardgame-core";
|
|
||||||
import {
|
|
||||||
type InventoryItem,
|
|
||||||
type GameItemMeta,
|
|
||||||
type RunState,
|
|
||||||
type GridInventory,
|
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
|
||||||
import type { Spawner } from "boardgame-phaser";
|
|
||||||
|
|
||||||
const ITEM_COLORS = [
|
|
||||||
0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33,
|
|
||||||
0xff6633,
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface InventoryItemSpawnerOptions {
|
|
||||||
scene: Phaser.Scene;
|
|
||||||
gameState: MutableSignal<RunState>;
|
|
||||||
parentContainer: Phaser.GameObjects.Container;
|
|
||||||
cellSize: number;
|
|
||||||
gridGap: number;
|
|
||||||
gridX: number;
|
|
||||||
gridY: number;
|
|
||||||
isLocked: () => boolean;
|
|
||||||
isDragging: () => boolean;
|
|
||||||
onItemDragStart: (
|
|
||||||
itemId: string,
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
itemContainer: Phaser.GameObjects.Container,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spawner for inventory items using the boardgame-phaser Spawner pattern.
|
|
||||||
* Reactively spawns/despawns/updates item visuals when gameState.inventory changes.
|
|
||||||
*
|
|
||||||
* Items currently being dragged are excluded from getData() to prevent
|
|
||||||
* the spawner from respawning them while they're in flight.
|
|
||||||
*/
|
|
||||||
export class InventoryItemSpawner implements Spawner<
|
|
||||||
InventoryItem<GameItemMeta>,
|
|
||||||
Phaser.GameObjects.Container
|
|
||||||
> {
|
|
||||||
private scene: Phaser.Scene;
|
|
||||||
private gameState: MutableSignal<RunState>;
|
|
||||||
private parentContainer: Phaser.GameObjects.Container;
|
|
||||||
private cellSize: number;
|
|
||||||
private gridGap: number;
|
|
||||||
private gridX: number;
|
|
||||||
private gridY: number;
|
|
||||||
private isLocked: () => boolean;
|
|
||||||
private isDragging: () => boolean;
|
|
||||||
private onItemDragStart: (
|
|
||||||
itemId: string,
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
itemContainer: Phaser.GameObjects.Container,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
private colorMap = new Map<string, number>();
|
|
||||||
private colorIdx = 0;
|
|
||||||
private draggingIds = new Set<string>();
|
|
||||||
|
|
||||||
constructor(options: InventoryItemSpawnerOptions) {
|
|
||||||
this.scene = options.scene;
|
|
||||||
this.gameState = options.gameState;
|
|
||||||
this.parentContainer = options.parentContainer;
|
|
||||||
this.cellSize = options.cellSize;
|
|
||||||
this.gridGap = options.gridGap;
|
|
||||||
this.gridX = options.gridX;
|
|
||||||
this.gridY = options.gridY;
|
|
||||||
this.isLocked = options.isLocked;
|
|
||||||
this.isDragging = options.isDragging;
|
|
||||||
this.onItemDragStart = options.onItemDragStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
*getData(): Iterable<InventoryItem<GameItemMeta>> {
|
|
||||||
const inventory = this.getInventory();
|
|
||||||
for (const [, item] of inventory.items) {
|
|
||||||
if (!this.draggingIds.has(item.id)) {
|
|
||||||
yield item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getKey(item: InventoryItem<GameItemMeta>): string {
|
|
||||||
return item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSpawn(item: InventoryItem<GameItemMeta>): Phaser.GameObjects.Container {
|
|
||||||
const color =
|
|
||||||
this.colorMap.get(item.id) ??
|
|
||||||
ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length];
|
|
||||||
this.colorMap.set(item.id, color);
|
|
||||||
|
|
||||||
const container = this.createItemVisuals(item, color);
|
|
||||||
this.setupInteraction(item, container);
|
|
||||||
this.parentContainer.add(container);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDespawn(
|
|
||||||
obj: Phaser.GameObjects.Container,
|
|
||||||
_item: InventoryItem<GameItemMeta>,
|
|
||||||
): void {
|
|
||||||
obj.removeAllListeners();
|
|
||||||
obj.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate(
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
obj: Phaser.GameObjects.Container,
|
|
||||||
): void {
|
|
||||||
const color = this.colorMap.get(item.id) ?? 0x888888;
|
|
||||||
this.rebuildItemVisuals(obj, item, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark an item as being dragged so the spawner excludes it from getData().
|
|
||||||
* Call this before removing the item from the inventory.
|
|
||||||
*/
|
|
||||||
markDragging(itemId: string): void {
|
|
||||||
this.draggingIds.add(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unmark an item after drag ends (placed or lost).
|
|
||||||
*/
|
|
||||||
unmarkDragging(itemId: string): void {
|
|
||||||
this.draggingIds.delete(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the color assigned to an item (creates one if not yet assigned).
|
|
||||||
*/
|
|
||||||
getItemColor(itemId: string): number {
|
|
||||||
if (!this.colorMap.has(itemId)) {
|
|
||||||
this.colorMap.set(
|
|
||||||
itemId,
|
|
||||||
ITEM_COLORS[this.colorIdx++ % ITEM_COLORS.length],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.colorMap.get(itemId)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInventory(): GridInventory<GameItemMeta> {
|
|
||||||
return this.gameState.value
|
|
||||||
.inventory as unknown as GridInventory<GameItemMeta>;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createItemVisuals(
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
color: number,
|
|
||||||
): Phaser.GameObjects.Container {
|
|
||||||
const container = this.scene.add.container(0, 0);
|
|
||||||
|
|
||||||
const graphics = this.scene.add.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);
|
|
||||||
|
|
||||||
container.add(graphics);
|
|
||||||
container.add(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,
|
|
||||||
);
|
|
||||||
container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
private rebuildItemVisuals(
|
|
||||||
container: Phaser.GameObjects.Container,
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
color: number,
|
|
||||||
): void {
|
|
||||||
container.removeAll(true);
|
|
||||||
|
|
||||||
const graphics = this.scene.add.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);
|
|
||||||
|
|
||||||
container.add(graphics);
|
|
||||||
container.add(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,
|
|
||||||
);
|
|
||||||
container.setInteractive(hitRect, Phaser.Geom.Rectangle.Contains);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupInteraction(
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
container: Phaser.GameObjects.Container,
|
|
||||||
): void {
|
|
||||||
container.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
|
||||||
// Guard against stale events firing on destroyed containers
|
|
||||||
if (!container.scene || !container.active) return;
|
|
||||||
if (this.isLocked()) return;
|
|
||||||
if (this.isDragging()) return;
|
|
||||||
if (pointer.button === 0) {
|
|
||||||
this.onItemDragStart(item.id, item, 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
import Phaser from "phaser";
|
|
||||||
import { MutableSignal } from "boardgame-core";
|
|
||||||
import { spawnEffect } from "boardgame-phaser";
|
|
||||||
import {
|
|
||||||
type GridInventory,
|
|
||||||
type InventoryItem,
|
|
||||||
type GameItemMeta,
|
|
||||||
type RunState,
|
|
||||||
removeItemFromGrid,
|
|
||||||
placeItem,
|
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
|
||||||
import { InventoryItemSpawner } from "./InventoryItemSpawner";
|
|
||||||
import { GridBackgroundRenderer } from "./GridBackgroundRenderer";
|
|
||||||
import { DragController } from "./DragController";
|
|
||||||
import { LostItemManager } from "./LostItemManager";
|
|
||||||
|
|
||||||
export interface InventoryWidgetOptions {
|
|
||||||
scene: Phaser.Scene;
|
|
||||||
gameState: MutableSignal<RunState>;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
cellSize: number;
|
|
||||||
gridGap?: number;
|
|
||||||
isLocked?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inventory widget using the Spawner pattern for reactive item rendering.
|
|
||||||
*
|
|
||||||
* Architecture:
|
|
||||||
* - InventoryItemSpawner + spawnEffect: reactive spawn/despawn/update of item visuals
|
|
||||||
* - GridBackgroundRenderer: static grid background drawn once
|
|
||||||
* - DragController: event-driven drag logic via dragDropEventEffect
|
|
||||||
* - LostItemManager: tracks items dropped outside valid placement
|
|
||||||
*/
|
|
||||||
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 itemSpawner: InventoryItemSpawner;
|
|
||||||
private backgroundRenderer: GridBackgroundRenderer;
|
|
||||||
private dragController: DragController;
|
|
||||||
private lostItemManager: LostItemManager;
|
|
||||||
|
|
||||||
private spawnDispose: (() => void) | null = null;
|
|
||||||
private rightClickHandler!: (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.getInventory();
|
|
||||||
|
|
||||||
this.container = this.scene.add.container(options.x, options.y);
|
|
||||||
|
|
||||||
// 1. Static grid background (drawn once)
|
|
||||||
this.backgroundRenderer = new GridBackgroundRenderer({
|
|
||||||
scene: this.scene,
|
|
||||||
parentContainer: this.container,
|
|
||||||
cellSize: this.cellSize,
|
|
||||||
gridGap: this.gridGap,
|
|
||||||
gridX: this.gridX,
|
|
||||||
gridY: this.gridY,
|
|
||||||
});
|
|
||||||
this.backgroundRenderer.draw(inventory.width, inventory.height);
|
|
||||||
|
|
||||||
// 2. Reactive item spawner
|
|
||||||
this.itemSpawner = new InventoryItemSpawner({
|
|
||||||
scene: this.scene,
|
|
||||||
gameState: this.gameState,
|
|
||||||
parentContainer: this.container,
|
|
||||||
cellSize: this.cellSize,
|
|
||||||
gridGap: this.gridGap,
|
|
||||||
gridX: this.gridX,
|
|
||||||
gridY: this.gridY,
|
|
||||||
isLocked: () => this.isLocked,
|
|
||||||
isDragging: () => this.dragController.isDragging(),
|
|
||||||
onItemDragStart: (itemId, item, itemContainer) => {
|
|
||||||
this.handleItemDragStart(itemId, item, itemContainer);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Drag controller
|
|
||||||
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.itemSpawner.getItemColor(id),
|
|
||||||
onPlaceItem: (item) => this.handlePlaceItem(item),
|
|
||||||
onCreateLostItem: (id, shape, transform, meta, x, y) =>
|
|
||||||
this.handleCreateLostItem(id, shape, transform, meta, x, y),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Lost item manager
|
|
||||||
this.lostItemManager = new LostItemManager({
|
|
||||||
scene: this.scene,
|
|
||||||
cellSize: this.cellSize,
|
|
||||||
gridGap: this.gridGap,
|
|
||||||
getItemColor: (id) => this.itemSpawner.getItemColor(id),
|
|
||||||
onLostItemDragStart: (id, lostContainer) =>
|
|
||||||
this.dragController.startLostItemDrag(
|
|
||||||
id,
|
|
||||||
this.getLostItemShape(id),
|
|
||||||
this.getLostItemTransform(id),
|
|
||||||
this.getLostItemMeta(id),
|
|
||||||
lostContainer,
|
|
||||||
),
|
|
||||||
isDragging: () => this.dragController.isDragging(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Activate the spawner effect (auto-cleans up on dispose)
|
|
||||||
this.spawnDispose = spawnEffect(this.itemSpawner);
|
|
||||||
|
|
||||||
// Right-click rotation handler
|
|
||||||
this.setupInput();
|
|
||||||
|
|
||||||
this.scene.events.once("shutdown", () => this.destroy());
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInventory(): GridInventory<GameItemMeta> {
|
|
||||||
return this.gameState.value
|
|
||||||
.inventory as unknown as GridInventory<GameItemMeta>;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleItemDragStart(
|
|
||||||
itemId: string,
|
|
||||||
item: InventoryItem<GameItemMeta>,
|
|
||||||
itemContainer: Phaser.GameObjects.Container,
|
|
||||||
): void {
|
|
||||||
// Mark as dragging FIRST so spawner excludes it from getData().
|
|
||||||
// This prevents the spawner effect from destroying the container
|
|
||||||
// when we later update the inventory state.
|
|
||||||
this.itemSpawner.markDragging(itemId);
|
|
||||||
|
|
||||||
// Start drag session
|
|
||||||
this.dragController.startDrag(itemId, item, itemContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handlePlaceItem(item: InventoryItem<GameItemMeta>): void {
|
|
||||||
this.gameState.produce((state) => {
|
|
||||||
placeItem(state.inventory, item);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unmark dragging so spawner picks it up on next effect run
|
|
||||||
this.itemSpawner.unmarkDragging(item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCreateLostItem(
|
|
||||||
itemId: string,
|
|
||||||
shape: InventoryItem<GameItemMeta>["shape"],
|
|
||||||
transform: InventoryItem<GameItemMeta>["transform"],
|
|
||||||
meta: InventoryItem<GameItemMeta>["meta"],
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
): void {
|
|
||||||
// Remove from inventory since it's dropped outside valid placement
|
|
||||||
this.gameState.produce((state) => {
|
|
||||||
removeItemFromGrid(state.inventory, itemId);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.lostItemManager.createLostItem(itemId, shape, transform, meta, x, y);
|
|
||||||
|
|
||||||
// Unmark dragging — item is now "lost" and managed by LostItemManager
|
|
||||||
this.itemSpawner.unmarkDragging(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLostItemShape(itemId: string) {
|
|
||||||
return this.lostItemManager.getLostItem(itemId)?.shape!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLostItemTransform(itemId: string) {
|
|
||||||
return this.lostItemManager.getLostItem(itemId)?.transform!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLostItemMeta(itemId: string) {
|
|
||||||
return this.lostItemManager.getLostItem(itemId)?.meta!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupInput(): void {
|
|
||||||
this.rightClickHandler = (pointer: Phaser.Input.Pointer) => {
|
|
||||||
if (!this.dragController.isDragging()) return;
|
|
||||||
if (pointer.button === 1) {
|
|
||||||
this.dragController.rotateDraggedItem();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.scene.input.on("pointerdown", this.rightClickHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setLocked(locked: boolean): void {
|
|
||||||
this.isLocked = locked;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLostItems(): string[] {
|
|
||||||
return this.lostItemManager.getLostItemIds();
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearLostItems(): void {
|
|
||||||
this.lostItemManager.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force re-sync of item visuals with current inventory state.
|
|
||||||
* With spawnEffect this is usually automatic, but useful after
|
|
||||||
* external state changes that don't trigger the effect.
|
|
||||||
*/
|
|
||||||
public refresh(): void {
|
|
||||||
// The spawner effect automatically re-syncs when gameState.value changes.
|
|
||||||
// If immediate refresh is needed, reading the signal triggers the effect.
|
|
||||||
void this.gameState.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.scene.input.off("pointerdown", this.rightClickHandler);
|
|
||||||
|
|
||||||
if (this.spawnDispose) {
|
|
||||||
this.spawnDispose();
|
|
||||||
this.spawnDispose = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dragController.destroy();
|
|
||||||
this.lostItemManager.destroy();
|
|
||||||
this.backgroundRenderer.destroy();
|
|
||||||
this.container.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue