Refactor inventory drag to use dragDropEventEffect

Replace manual pointer event handling with the framework's
dragDropEventEffect utility. Update DragController to manage
DisposableBag for cleanup and pass containers instead of
pointers to drag callbacks. Add framework path alias to
tsconfig and fix loop variable shadowing in LostItemManager.
This commit is contained in:
hypercross 2026-04-19 00:12:56 +08:00
parent 88d0c5bf55
commit a7095c37fc
5 changed files with 308 additions and 274 deletions

View File

@ -1,13 +1,13 @@
import { h } from 'preact';
import { PhaserGame, PhaserScene } from 'boardgame-phaser';
import { useMemo } from 'preact/hooks';
import { IndexScene } from '@/scenes/IndexScene';
import { MapViewerScene } from '@/scenes/MapViewerScene';
import { GridViewerScene } from '@/scenes/GridViewerScene';
import { ShapeViewerScene } from '@/scenes/ShapeViewerScene';
import { GameFlowScene } from '@/scenes/GameFlowScene';
import { PlaceholderEncounterScene } from '@/scenes/PlaceholderEncounterScene';
import { createGameState } from '@/state/gameState';
import { h } from "preact";
import { PhaserGame, PhaserScene } from "boardgame-phaser";
import { useMemo } from "preact/hooks";
import { IndexScene } from "@/scenes/IndexScene";
import { MapViewerScene } from "@/scenes/MapViewerScene";
import { GridViewerScene } from "@/scenes/GridViewerScene";
import { ShapeViewerScene } from "@/scenes/ShapeViewerScene";
import { GameFlowScene } from "@/scenes/GameFlowScene";
import { PlaceholderEncounterScene } from "@/scenes/PlaceholderEncounterScene";
import { createGameState } from "@/state/gameState";
// 全局游戏状态单例
const gameState = createGameState();
@ -18,18 +18,36 @@ export default function App() {
const gridViewerScene = useMemo(() => new GridViewerScene(), []);
const shapeViewerScene = useMemo(() => new ShapeViewerScene(), []);
const gameFlowScene = useMemo(() => new GameFlowScene(gameState), []);
const placeholderEncounterScene = useMemo(() => new PlaceholderEncounterScene(gameState), []);
const placeholderEncounterScene = useMemo(
() => new PlaceholderEncounterScene(gameState),
[],
);
return (
<div className="flex flex-col h-screen">
<div className="flex-1 flex relative justify-center items-center">
<PhaserGame initialScene="IndexScene" config={{ width: 1920, height: 1080 }}>
<PhaserScene sceneKey="IndexScene" scene={indexScene} />
<PhaserScene sceneKey="MapViewerScene" scene={mapViewerScene} />
<PhaserScene sceneKey="GridViewerScene" scene={gridViewerScene} />
<PhaserScene sceneKey="ShapeViewerScene" scene={shapeViewerScene} />
<PhaserScene sceneKey="GameFlowScene" scene={gameFlowScene} />
<PhaserScene sceneKey="PlaceholderEncounterScene" scene={placeholderEncounterScene} />
<PhaserGame
initialScene="IndexScene"
config={{ width: 1920, height: 1080 }}
>
<PhaserScene sceneKey="IndexScene" scene={indexScene as any} />
<PhaserScene
sceneKey="MapViewerScene"
scene={mapViewerScene as any}
/>
<PhaserScene
sceneKey="GridViewerScene"
scene={gridViewerScene as any}
/>
<PhaserScene
sceneKey="ShapeViewerScene"
scene={shapeViewerScene as any}
/>
<PhaserScene sceneKey="GameFlowScene" scene={gameFlowScene as any} />
<PhaserScene
sceneKey="PlaceholderEncounterScene"
scene={placeholderEncounterScene as any}
/>
</PhaserGame>
</div>
</div>

View File

@ -4,19 +4,19 @@ import {
type GameItemMeta,
type GridInventory,
validatePlacement,
placeItem,
transformShape,
} from "boardgame-core/samples/slay-the-spire-like";
import { dragDropEventEffect, DragDropEventType } from "boardgame-phaser";
import { DisposableBag } from "boardgame-phaser";
export interface DragState {
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;
dragOffsetX: number;
dragOffsetY: number;
disposables: DisposableBag;
}
export interface DragControllerOptions {
@ -28,21 +28,20 @@ export interface DragControllerOptions {
gridY: number;
getInventory: () => GridInventory<GameItemMeta>;
getItemColor: (itemId: string) => number;
getItemCells: (
item: InventoryItem<GameItemMeta>,
) => { x: number; y: 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;
}
/**
* Manages drag-and-drop state and logic for inventory items.
* Handles ghost visuals, placement preview, rotation, and validation.
* Event-driven drag controller using dragDropEventEffect from boardgame-phaser.
* Manages ghost visuals, placement preview, rotation, and validation.
*/
export class DragController {
private scene: Phaser.Scene;
@ -53,18 +52,17 @@ export class DragController {
private gridY: number;
private getInventory: () => GridInventory<GameItemMeta>;
private getItemColor: (itemId: string) => number;
private getItemCells: (
item: InventoryItem<GameItemMeta>,
) => { x: number; y: 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 dragState: DragState | null = null;
private activeSession: DragSession | null = null;
constructor(options: DragControllerOptions) {
this.scene = options.scene;
@ -75,66 +73,44 @@ export class DragController {
this.gridY = options.gridY;
this.getInventory = options.getInventory;
this.getItemColor = options.getItemColor;
this.getItemCells = options.getItemCells;
this.onPlaceItem = options.onPlaceItem;
this.onCreateLostItem = options.onCreateLostItem;
}
/**
* Start dragging an item from the inventory.
* Start a drag session for an inventory item.
* Uses dragDropEventEffect for pointer tracking and event emission.
*/
startDrag(itemId: string, pointer: Phaser.Input.Pointer): void {
const inventory = this.getInventory();
const item = inventory.items.get(itemId);
if (!item) return;
startDrag(
itemId: string,
item: InventoryItem<GameItemMeta>,
itemContainer: Phaser.GameObjects.Container,
): () => void {
const cells = this.getItemCells(item);
const firstCell = cells[0];
const itemWorldX =
const worldX =
this.container.x +
this.gridX +
firstCell.x * (this.cellSize + this.gridGap);
const itemWorldY =
const worldY =
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);
const ghostGraphics = this.scene.add.graphics();
const color = this.getItemColor(itemId);
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 ghostContainer = this.createGhostContainer(
worldX,
worldY,
item.shape,
item.transform,
this.getItemColor(itemId),
);
const previewGraphics = this.scene.add
.graphics()
.setDepth(999)
.setAlpha(0.5);
this.dragState = {
const disposables = new DisposableBag();
const session: DragSession = {
itemId,
itemShape: item.shape,
itemTransform: {
@ -144,26 +120,160 @@ export class DragController {
itemMeta: item.meta,
ghostContainer,
previewGraphics,
dragOffsetX,
dragOffsetY,
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 dragging a lost item (item that was dropped outside valid placement).
* Start a drag session for a lost item.
*/
startLostItemDrag(
itemId: string,
shape: InventoryItem<GameItemMeta>["shape"],
transform: InventoryItem<GameItemMeta>["transform"],
meta: InventoryItem<GameItemMeta>["meta"],
pointer: Phaser.Input.Pointer,
): void {
const ghostContainer = this.scene.add
.container(pointer.x, pointer.y)
.setDepth(1000);
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 color = this.getItemColor(itemId);
const cells = transformShape(shape, transform);
for (const cell of cells) {
@ -184,52 +294,15 @@ export class DragController {
}
ghostContainer.add(ghostGraphics);
const previewGraphics = this.scene.add
.graphics()
.setDepth(999)
.setAlpha(0.5);
this.dragState = {
itemId,
itemShape: shape,
itemTransform: { ...transform, offset: { ...transform.offset } },
itemMeta: meta,
ghostContainer,
previewGraphics,
dragOffsetX: 0,
dragOffsetY: 0,
};
return ghostContainer;
}
/**
* Rotate the currently dragged item by 90 degrees.
*/
rotateDraggedItem(): void {
if (!this.dragState) return;
const currentRotation = (this.dragState.itemTransform.rotation + 90) % 360;
this.dragState.itemTransform = {
...this.dragState.itemTransform,
rotation: currentRotation,
};
this.updateGhostVisuals();
}
/**
* Update ghost visuals to reflect current drag state (after rotation).
*/
private updateGhostVisuals(): void {
if (!this.dragState) return;
this.dragState.ghostContainer.removeAll(true);
private updateGhostVisuals(session: DragSession): void {
session.ghostContainer.removeAll(true);
const ghostGraphics = this.scene.add.graphics();
const color = this.getItemColor(this.dragState.itemId);
const color = this.getItemColor(session.itemId);
const cells = transformShape(
this.dragState.itemShape,
this.dragState.itemTransform,
);
const cells = transformShape(session.itemShape, session.itemTransform);
for (const cell of cells) {
ghostGraphics.fillStyle(color, 0.7);
ghostGraphics.fillRect(
@ -246,68 +319,58 @@ export class DragController {
this.cellSize,
);
}
this.dragState.ghostContainer.add(ghostGraphics);
session.ghostContainer.add(ghostGraphics);
}
/**
* Handle pointer movement during drag: update ghost position and placement preview.
*/
onPointerMove(pointer: Phaser.Input.Pointer): void {
if (!this.dragState) return;
private handleDragMove(session: DragSession): void {
const pointer = this.scene.input.activePointer;
session.ghostContainer.setPosition(pointer.x, pointer.y);
this.dragState.ghostContainer.setPosition(
pointer.x - this.dragState.dragOffsetX,
pointer.y - this.dragState.dragOffsetY,
);
const gridCell = this.getWorldGridCell(
pointer.x - this.dragState.dragOffsetX,
pointer.y - this.dragState.dragOffsetY,
);
this.dragState.previewGraphics.clear();
const gridCell = this.getWorldGridCell(pointer.x, pointer.y);
session.previewGraphics.clear();
if (gridCell) {
const inventory = this.getInventory();
const testTransform = {
...this.dragState.itemTransform,
...session.itemTransform,
offset: { x: gridCell.x, y: gridCell.y },
};
const validation = validatePlacement(
inventory,
this.dragState.itemShape,
session.itemShape,
testTransform,
);
const cells = transformShape(this.dragState.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) {
this.dragState.previewGraphics.fillStyle(0x33ff33, 0.3);
this.dragState.previewGraphics.fillRect(
session.previewGraphics.fillStyle(0x33ff33, 0.3);
session.previewGraphics.fillRect(
px,
py,
this.cellSize,
this.cellSize,
);
this.dragState.previewGraphics.lineStyle(2, 0x33ff33);
this.dragState.previewGraphics.strokeRect(
session.previewGraphics.lineStyle(2, 0x33ff33);
session.previewGraphics.strokeRect(
px,
py,
this.cellSize,
this.cellSize,
);
} else {
this.dragState.previewGraphics.fillStyle(0xff3333, 0.3);
this.dragState.previewGraphics.fillRect(
session.previewGraphics.fillStyle(0xff3333, 0.3);
session.previewGraphics.fillRect(
px,
py,
this.cellSize,
this.cellSize,
);
this.dragState.previewGraphics.lineStyle(2, 0xff3333);
this.dragState.previewGraphics.strokeRect(
session.previewGraphics.lineStyle(2, 0xff3333);
session.previewGraphics.strokeRect(
px,
py,
this.cellSize,
@ -318,63 +381,56 @@ export class DragController {
}
}
/**
* Handle pointer release: validate placement and either place item or create lost item.
*/
onPointerUp(pointer: Phaser.Input.Pointer): void {
if (!this.dragState) return;
const gridCell = this.getWorldGridCell(
pointer.x - this.dragState.dragOffsetX,
pointer.y - this.dragState.dragOffsetY,
);
private handleDragEnd(session: DragSession): void {
const pointer = this.scene.input.activePointer;
const gridCell = this.getWorldGridCell(pointer.x, pointer.y);
const inventory = this.getInventory();
this.dragState.ghostContainer.destroy();
this.dragState.previewGraphics.destroy();
session.ghostContainer.destroy();
session.previewGraphics.destroy();
session.disposables.dispose();
if (gridCell) {
const testTransform = {
...this.dragState.itemTransform,
...session.itemTransform,
offset: { x: gridCell.x, y: gridCell.y },
};
const validation = validatePlacement(
inventory,
this.dragState.itemShape,
session.itemShape,
testTransform,
);
if (validation.valid) {
const item: InventoryItem<GameItemMeta> = {
id: this.dragState.itemId,
shape: this.dragState.itemShape,
id: session.itemId,
shape: session.itemShape,
transform: testTransform,
meta: this.dragState.itemMeta,
meta: session.itemMeta,
};
this.onPlaceItem(item);
} else {
this.onCreateLostItem(
this.dragState.itemId,
this.dragState.itemShape,
this.dragState.itemTransform,
this.dragState.itemMeta,
session.itemId,
session.itemShape,
session.itemTransform,
session.itemMeta,
session.ghostContainer.x,
session.ghostContainer.y,
);
}
} else {
this.onCreateLostItem(
this.dragState.itemId,
this.dragState.itemShape,
this.dragState.itemTransform,
this.dragState.itemMeta,
session.itemId,
session.itemShape,
session.itemTransform,
session.itemMeta,
session.ghostContainer.x,
session.ghostContainer.y,
);
}
this.dragState = null;
}
/**
* Convert world coordinates to grid cell coordinates.
*/
private getWorldGridCell(
worldX: number,
worldY: number,
@ -398,36 +454,24 @@ export class DragController {
return { x: cellX, y: cellY };
}
/**
* Check if an item is currently being dragged.
*/
isDragging(): boolean {
return this.dragState !== null;
}
/**
* Get the ID of the item being dragged, or null.
*/
getDraggedItemId(): string | null {
return this.dragState?.itemId ?? null;
}
getDraggedItemPosition(): { x: number; y: number } {
if (!this.dragState) return { x: 0, y: 0 };
return {
x: this.dragState.ghostContainer.x,
y: this.dragState.ghostContainer.y,
};
}
/**
* Clean up drag state and visuals.
*/
destroy(): void {
if (this.dragState) {
this.dragState.ghostContainer.destroy();
this.dragState.previewGraphics.destroy();
this.dragState = null;
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();
}
}

View File

@ -25,6 +25,7 @@ export interface InventoryWidgetOptions {
/**
* Thin orchestrator for the inventory grid widget.
* Delegates rendering, drag logic, and lost-item management to focused modules.
* Uses event-driven drag via dragDropEventEffect from boardgame-phaser.
*/
export class InventoryWidget {
private scene: Phaser.Scene;
@ -40,9 +41,7 @@ export class InventoryWidget {
private dragController: DragController;
private lostItemManager: LostItemManager;
private pointerMoveHandler!: (pointer: Phaser.Input.Pointer) => void;
private pointerUpHandler!: (pointer: Phaser.Input.Pointer) => void;
private pointerDownHandler!: (pointer: Phaser.Input.Pointer) => void;
private rightClickHandler!: (pointer: Phaser.Input.Pointer) => void;
constructor(options: InventoryWidgetOptions) {
this.scene = options.scene;
@ -52,14 +51,9 @@ export class InventoryWidget {
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);
// Initialize sub-modules
this.renderer = new ItemRenderer({
scene: this.scene,
container: this.container,
@ -78,10 +72,9 @@ export class InventoryWidget {
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),
onCreateLostItem: (id, shape, transform, meta, x, y) =>
this.handleCreateLostItem(id, shape, transform, meta, x, y),
});
this.lostItemManager = new LostItemManager({
@ -89,13 +82,13 @@ export class InventoryWidget {
cellSize: this.cellSize,
gridGap: this.gridGap,
getItemColor: (id) => this.renderer.getItemColor(id),
onLostItemDragStart: (id, pointer) =>
onLostItemDragStart: (id, lostContainer) =>
this.dragController.startLostItemDrag(
id,
this.getLostItemShape(id),
this.getLostItemTransform(id),
this.getLostItemMeta(id),
pointer,
lostContainer,
),
isDragging: () => this.dragController.isDragging(),
});
@ -117,13 +110,14 @@ export class InventoryWidget {
for (const [itemId, item] of inventory.items) {
if (this.renderer.hasItem(itemId)) continue;
const visuals = this.renderer.createItemVisuals(itemId, item);
this.setupItemInteraction(itemId, visuals);
this.setupItemInteraction(itemId, visuals, item);
}
}
private setupItemInteraction(
itemId: string,
visuals: ReturnType<typeof ItemRenderer.prototype.createItemVisuals>,
item: InventoryItem<GameItemMeta>,
): void {
visuals.container.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
if (this.isLocked) return;
@ -133,30 +127,20 @@ export class InventoryWidget {
removeItemFromGrid(state.inventory, itemId);
});
this.renderer.removeItemVisuals(itemId);
this.dragController.startDrag(itemId, pointer);
this.dragController.startDrag(itemId, item, visuals.container);
}
});
}
private setupInput(): void {
this.pointerDownHandler = (pointer: Phaser.Input.Pointer) => {
this.rightClickHandler = (pointer: Phaser.Input.Pointer) => {
if (!this.dragController.isDragging()) return;
if (pointer.button === 1) {
this.dragController.rotateDraggedItem();
}
};
this.pointerMoveHandler = (pointer: Phaser.Input.Pointer) => {
this.dragController.onPointerMove(pointer);
};
this.pointerUpHandler = (pointer: Phaser.Input.Pointer) => {
this.dragController.onPointerUp(pointer);
};
this.scene.input.on("pointermove", this.pointerMoveHandler);
this.scene.input.on("pointerup", this.pointerUpHandler);
this.scene.input.on("pointerdown", this.pointerDownHandler);
this.scene.input.on("pointerdown", this.rightClickHandler);
}
private handlePlaceItem(item: InventoryItem<GameItemMeta>): void {
@ -167,7 +151,7 @@ export class InventoryWidget {
const placedItem = inventory.items.get(item.id);
if (placedItem) {
const visuals = this.renderer.createItemVisuals(item.id, placedItem);
this.setupItemInteraction(item.id, visuals);
this.setupItemInteraction(item.id, visuals, placedItem);
}
}
@ -176,15 +160,10 @@ export class InventoryWidget {
shape: InventoryItem<GameItemMeta>["shape"],
transform: InventoryItem<GameItemMeta>["transform"],
meta: InventoryItem<GameItemMeta>["meta"],
x: number,
y: number,
): void {
this.lostItemManager.createLostItem(
itemId,
shape,
transform,
meta,
this.dragController.getDraggedItemPosition().x,
this.dragController.getDraggedItemPosition().y,
);
this.lostItemManager.createLostItem(itemId, shape, transform, meta, x, y);
}
private getLostItemShape(itemId: string) {
@ -214,13 +193,6 @@ export class InventoryWidget {
public refresh(): void {
const inventory = this.getInventory();
// 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,
@ -235,9 +207,7 @@ export class InventoryWidget {
}
public destroy(): void {
this.scene.input.off("pointermove", this.pointerMoveHandler);
this.scene.input.off("pointerup", this.pointerUpHandler);
this.scene.input.off("pointerdown", this.pointerDownHandler);
this.scene.input.off("pointerdown", this.rightClickHandler);
this.dragController.destroy();
this.lostItemManager.destroy();

View File

@ -17,7 +17,10 @@ export interface LostItemManagerOptions {
cellSize: number;
gridGap: number;
getItemColor: (itemId: string) => number;
onLostItemDragStart: (itemId: string, pointer: Phaser.Input.Pointer) => void;
onLostItemDragStart: (
itemId: string,
container: Phaser.GameObjects.Container,
) => void;
isDragging: () => boolean;
}
@ -32,7 +35,7 @@ export class LostItemManager {
private getItemColor: (itemId: string) => number;
private onLostItemDragStart: (
itemId: string,
pointer: Phaser.Input.Pointer,
container: Phaser.GameObjects.Container,
) => void;
private isDragging: () => boolean;
@ -48,37 +51,35 @@ export class LostItemManager {
}
/**
* Create a visual representation of a lost item.
* Create a visual representation of a lost item at the given position.
*/
createLostItem(
itemId: string,
shape: InventoryItem<GameItemMeta>["shape"],
transform: InventoryItem<GameItemMeta>["transform"],
meta: InventoryItem<GameItemMeta>["meta"],
positionX: number,
positionY: number,
x: number,
y: number,
): void {
const container = this.scene.add
.container(positionX, positionY)
.setDepth(500);
const container = this.scene.add.container(x, y).setDepth(500);
const graphics = this.scene.add.graphics();
const color = this.getItemColor(itemId);
for (let y = 0; y < shape.height; y++) {
for (let x = 0; x < shape.width; x++) {
if (shape.grid[y]?.[x]) {
for (let gy = 0; gy < shape.height; gy++) {
for (let gx = 0; gx < shape.width; gx++) {
if (shape.grid[gy]?.[gx]) {
graphics.fillStyle(color, 0.5);
graphics.fillRect(
x * (this.cellSize + this.gridGap),
y * (this.cellSize + this.gridGap),
gx * (this.cellSize + this.gridGap),
gy * (this.cellSize + this.gridGap),
this.cellSize - 2,
this.cellSize - 2,
);
graphics.lineStyle(2, 0xff4444);
graphics.strokeRect(
x * (this.cellSize + this.gridGap),
y * (this.cellSize + this.gridGap),
gx * (this.cellSize + this.gridGap),
gy * (this.cellSize + this.gridGap),
this.cellSize,
this.cellSize,
);
@ -108,7 +109,7 @@ export class LostItemManager {
container.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
if (this.isDragging()) return;
if (pointer.button === 0) {
this.onLostItemDragStart(itemId, pointer);
this.onLostItemDragStart(itemId, container);
}
});

View File

@ -3,14 +3,15 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["src/*"],
"boardgame-phaser": ["../framework/src/index.ts"],
},
"jsx": "react-jsx",
"jsxImportSource": "preact",
"noEmit": true,
"declaration": false,
"declarationMap": false,
"sourceMap": false
"sourceMap": false,
},
"include": ["src/**/*"]
"include": ["src/**/*"],
}