refactor: move InventoryItemSpawner and update grid drawing
- Move `InventoryItemSpawner` to `@/gameobjects` - Wrap grid drawing in `addEffect` in `InventoryTestScene` to ensure reactivity - Fix return type in `onMoveItem` callback
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import Phaser from "phaser";
|
||||
import { dragDropEventEffect, DragDropEventType } from "boardgame-phaser";
|
||||
import { GRID_CONFIG, ITEM_COLORS } from "@/config";
|
||||
import {
|
||||
IDENTITY_TRANSFORM,
|
||||
ParsedShape,
|
||||
Transform2D,
|
||||
transformShape,
|
||||
type GameItem,
|
||||
type GameItemMeta,
|
||||
type InventoryItem,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { computed, signal, Signal } from "@preact/signals-core";
|
||||
import { DisposableBag } from "../../../framework/dist";
|
||||
|
||||
export interface InventoryItemContainerCallbacks {
|
||||
onMoveItem: (itemId: string, newX: number, newY: number) => boolean;
|
||||
}
|
||||
|
||||
export class InventoryItemContainer extends Phaser.GameObjects.Container {
|
||||
private item: Signal<GameItem | undefined>;
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
private gridOffsetX: number,
|
||||
private gridOffsetY: number,
|
||||
private callbacks: InventoryItemContainerCallbacks,
|
||||
) {
|
||||
super(scene, gridOffsetX, gridOffsetY);
|
||||
scene.add.existing(this);
|
||||
this.setupInteractive();
|
||||
|
||||
const graphics = this.scene.add.graphics();
|
||||
const label = this.scene.add.text(
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||
"",
|
||||
GRID_CONFIG.ITEM_NAME_STYLE,
|
||||
);
|
||||
this.add([graphics, label]);
|
||||
|
||||
this.item = signal();
|
||||
|
||||
const disposables = new DisposableBag(this);
|
||||
|
||||
const itemName = computed(() => {
|
||||
const item = this.item.value;
|
||||
return item?.meta?.itemData.name ?? item?.id ?? "";
|
||||
});
|
||||
disposables.addEffect(() => {
|
||||
label.setText(itemName.value);
|
||||
});
|
||||
|
||||
const shape = computed(() => {
|
||||
return this.item.value?.shape;
|
||||
});
|
||||
const color = computed(() => {
|
||||
return this.getItemColor(this.item.value?.id ?? "");
|
||||
});
|
||||
disposables.addEffect(() => {
|
||||
graphics.clear();
|
||||
if (!shape.value) return;
|
||||
this.renderGraphics(graphics, shape.value, color.value);
|
||||
});
|
||||
|
||||
const transform = computed(() => {
|
||||
return this.item.value?.transform;
|
||||
});
|
||||
disposables.addEffect(() => {
|
||||
if (!transform.value) return;
|
||||
this.snapBack(transform.value);
|
||||
});
|
||||
}
|
||||
|
||||
setItem(item: GameItem) {
|
||||
this.item.value = item;
|
||||
}
|
||||
|
||||
renderGraphics(
|
||||
graphics: Phaser.GameObjects.Graphics,
|
||||
shape: ParsedShape,
|
||||
itemColor: number,
|
||||
): void {
|
||||
const cells = transformShape(shape, IDENTITY_TRANSFORM);
|
||||
for (const cell of cells) {
|
||||
const localX = (cell.x - cells[0].x) * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const localY = (cell.y - cells[0].y) * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
graphics.fillStyle(itemColor);
|
||||
graphics.fillRect(
|
||||
localX + 2,
|
||||
localY + 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||
);
|
||||
}
|
||||
|
||||
this.setSize(
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE * shape.width,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE * shape.height,
|
||||
);
|
||||
}
|
||||
|
||||
private getItemColor(itemId: string): number {
|
||||
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
return ITEM_COLORS[hash % ITEM_COLORS.length];
|
||||
}
|
||||
|
||||
private setupInteractive(): void {
|
||||
this.setScrollFactor(0);
|
||||
this.setInteractive({ useHandCursor: true });
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
dragDropEventEffect(this, (event) => {
|
||||
if (event.type === DragDropEventType.DOWN) {
|
||||
startX = this.x;
|
||||
startY = this.y;
|
||||
this.setAlpha(0.7);
|
||||
} else if (event.type === DragDropEventType.MOVE) {
|
||||
this.x = startX + event.deltaX;
|
||||
this.y = startY + event.deltaY;
|
||||
} else if (event.type === DragDropEventType.UP) {
|
||||
this.setAlpha(1);
|
||||
if (!this.handleDragEnd()) {
|
||||
this.x = startX;
|
||||
this.y = startY;
|
||||
}
|
||||
startX = startY = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleDragEnd(): boolean {
|
||||
const item = this.item.value;
|
||||
if (!item) return false;
|
||||
const itemId = item.id;
|
||||
|
||||
const cellSize = GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const shapeWidth = item.shape?.width ?? 1;
|
||||
const shapeHeight = item.shape?.height ?? 1;
|
||||
|
||||
const x = this.x - this.gridOffsetX;
|
||||
const y = this.y - this.gridOffsetY;
|
||||
const targetX = Math.round((x - cellSize / 2) / cellSize);
|
||||
const targetY = Math.round((y - cellSize / 2) / cellSize);
|
||||
|
||||
const clampedX = Math.max(0, Math.min(targetX, 10 - shapeWidth));
|
||||
const clampedY = Math.max(0, Math.min(targetY, 10 - shapeHeight));
|
||||
|
||||
if (
|
||||
clampedX !== item.transform.offset.x ||
|
||||
clampedY !== item.transform.offset.y
|
||||
) {
|
||||
return this.callbacks.onMoveItem(itemId, clampedX, clampedY);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private snapBack(transform: Transform2D): void {
|
||||
const { x, y } = transform.offset;
|
||||
const targetX = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const targetY = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
duration: 150,
|
||||
ease: "Power2",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import Phaser from "phaser";
|
||||
import type { Spawner } from "boardgame-phaser";
|
||||
import type {
|
||||
GameItemMeta,
|
||||
InventoryItem,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import type { InventorySignal } from "@/state/inventory";
|
||||
import { spawnEffect } from "boardgame-phaser";
|
||||
import { InventoryItemContainer } from "./InventoryItemContainer";
|
||||
import type { InventoryItemContainerCallbacks } from "./InventoryItemContainer";
|
||||
|
||||
export interface InventoryItemSpawnerCallbacks extends InventoryItemContainerCallbacks {}
|
||||
|
||||
export class InventoryItemSpawner implements Spawner<
|
||||
[string, InventoryItem<GameItemMeta>],
|
||||
InventoryItemContainer
|
||||
> {
|
||||
constructor(
|
||||
private scene: Phaser.Scene,
|
||||
private inventorySignal: InventorySignal,
|
||||
private gridOffsetX: number,
|
||||
private gridOffsetY: number,
|
||||
private callbacks: InventoryItemSpawnerCallbacks,
|
||||
) {}
|
||||
|
||||
*getData(): Iterable<[string, InventoryItem<GameItemMeta>]> {
|
||||
const inventory = this.inventorySignal.value;
|
||||
yield* inventory.items.entries();
|
||||
}
|
||||
|
||||
getKey(entry: [string, InventoryItem<GameItemMeta>]): string {
|
||||
return entry[0];
|
||||
}
|
||||
|
||||
onSpawn(
|
||||
entry: [string, InventoryItem<GameItemMeta>],
|
||||
): InventoryItemContainer | null {
|
||||
const [itemId, item] = entry;
|
||||
|
||||
const container = new InventoryItemContainer(
|
||||
this.scene,
|
||||
this.gridOffsetX,
|
||||
this.gridOffsetY,
|
||||
{
|
||||
onMoveItem: (id, newX, newY) => {
|
||||
return this.callbacks.onMoveItem(id, newX, newY);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
container.setItem(item);
|
||||
return container;
|
||||
}
|
||||
|
||||
onUpdate(
|
||||
entry: [string, InventoryItem<GameItemMeta>],
|
||||
container: InventoryItemContainer,
|
||||
): void {
|
||||
const [itemId, item] = entry;
|
||||
|
||||
container.setItem(item);
|
||||
}
|
||||
|
||||
onDespawn(container: InventoryItemContainer): void {
|
||||
// TODO: add tween
|
||||
container.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export function createInventoryItemSpawner(
|
||||
scene: Phaser.Scene,
|
||||
inventorySignal: InventorySignal,
|
||||
gridOffsetX: number,
|
||||
gridOffsetY: number,
|
||||
callbacks: InventoryItemSpawnerCallbacks,
|
||||
) {
|
||||
return spawnEffect(
|
||||
new InventoryItemSpawner(
|
||||
scene,
|
||||
inventorySignal,
|
||||
gridOffsetX,
|
||||
gridOffsetY,
|
||||
callbacks,
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user