refactor: implement inventory item spawner and config
- Add cell and text styling constants to `GRID_CONFIG` - Replace manual item drawing in `InventoryTestScene` with `InventoryItemSpawner` - Update `InventoryTestScene` to use dynamic inventory dimensions for grid rendering - Register `InventoryTestScene` in the main `App` component
This commit is contained in:
parent
b06876ccc0
commit
5884e74d8d
|
|
@ -33,6 +33,29 @@ export const GRID_CONFIG = {
|
||||||
WIDGET_CELL_SIZE: 80,
|
WIDGET_CELL_SIZE: 80,
|
||||||
/** Gap between grid cells (pixels) */
|
/** Gap between grid cells (pixels) */
|
||||||
GRID_GAP: 2,
|
GRID_GAP: 2,
|
||||||
|
/** Empty cell background color */
|
||||||
|
CELL_EMPTY_COLOR: 0x222233,
|
||||||
|
/** Occupied cell background color */
|
||||||
|
CELL_OCCUPIED_COLOR: 0x334455,
|
||||||
|
/** Grid line color */
|
||||||
|
GRID_LINE_COLOR: 0x555577,
|
||||||
|
/** Title text style */
|
||||||
|
TITLE_STYLE: {
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontStyle: "bold",
|
||||||
|
} as const,
|
||||||
|
/** Subtitle/hint text style */
|
||||||
|
SUBTITLE_STYLE: {
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#aaaaaa",
|
||||||
|
} as const,
|
||||||
|
/** Item name text style */
|
||||||
|
ITEM_NAME_STYLE: {
|
||||||
|
fontSize: "11px",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontStyle: "bold",
|
||||||
|
} as const,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ── Shape Viewer ────────────────────────────────────────────────────────────
|
// ── Shape Viewer ────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { spawnEffect, type Spawner } from "boardgame-phaser";
|
||||||
|
import { GRID_CONFIG, ITEM_COLORS } from "@/config";
|
||||||
|
import type {
|
||||||
|
GameItemMeta,
|
||||||
|
InventoryItem,
|
||||||
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
|
import type { InventorySignal } from "@/state/inventory";
|
||||||
|
|
||||||
|
export class InventoryItemSpawner implements Spawner<
|
||||||
|
[string, InventoryItem<GameItemMeta>],
|
||||||
|
Phaser.GameObjects.Container
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
private scene: Phaser.Scene,
|
||||||
|
private inventorySignal: InventorySignal,
|
||||||
|
private gridOffsetX: number,
|
||||||
|
private gridOffsetY: number,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
*getData(): Iterable<[string, InventoryItem<GameItemMeta>]> {
|
||||||
|
const inventory = this.inventorySignal.value;
|
||||||
|
yield* inventory.items.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey(entry: [string, InventoryItem<GameItemMeta>]): string {
|
||||||
|
return entry[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCells(
|
||||||
|
item: InventoryItem<GameItemMeta>,
|
||||||
|
): { x: number; y: number }[] {
|
||||||
|
const cells: { x: number; y: number }[] = [];
|
||||||
|
const shape = item.shape;
|
||||||
|
const transform = item.transform;
|
||||||
|
|
||||||
|
for (let y = 0; y < shape.height; y++) {
|
||||||
|
for (let x = 0; x < shape.width; x++) {
|
||||||
|
if (shape.grid[y]?.[x]) {
|
||||||
|
const finalX = x + transform.offset.x;
|
||||||
|
const finalY = y + transform.offset.y;
|
||||||
|
cells.push({ x: finalX, y: finalY });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSpawn(
|
||||||
|
entry: [string, InventoryItem<GameItemMeta>],
|
||||||
|
): Phaser.GameObjects.Container | null {
|
||||||
|
const [itemId, item] = entry;
|
||||||
|
const container = this.scene.add.container(0, 0);
|
||||||
|
const cells = this.getCells(item);
|
||||||
|
|
||||||
|
const itemColor = this.getItemColor(itemId);
|
||||||
|
const graphics = this.scene.add.graphics();
|
||||||
|
|
||||||
|
for (const cell of cells) {
|
||||||
|
const px = this.gridOffsetX + cell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
|
const py = this.gridOffsetY + cell.y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
|
|
||||||
|
graphics.fillStyle(itemColor);
|
||||||
|
graphics.fillRect(
|
||||||
|
px + 2,
|
||||||
|
py + 2,
|
||||||
|
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||||
|
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cells.length > 0) {
|
||||||
|
const firstCell = cells[0];
|
||||||
|
const px = this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
|
const py = this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
|
const itemName = item.meta?.itemData.name ?? item.id;
|
||||||
|
|
||||||
|
const text = this.scene.add.text(
|
||||||
|
px + GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||||
|
py + GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
||||||
|
itemName,
|
||||||
|
GRID_CONFIG.ITEM_NAME_STYLE,
|
||||||
|
);
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
|
||||||
|
container.add([graphics, text]);
|
||||||
|
} else {
|
||||||
|
container.add(graphics);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(
|
||||||
|
entry: [string, InventoryItem<GameItemMeta>],
|
||||||
|
obj: Phaser.GameObjects.Container,
|
||||||
|
): void {
|
||||||
|
const [, item] = entry;
|
||||||
|
const cells = this.getCells(item);
|
||||||
|
|
||||||
|
if (cells.length > 0) {
|
||||||
|
const firstCell = cells[0];
|
||||||
|
const px = this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
|
const py = this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
|
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: obj,
|
||||||
|
x: px,
|
||||||
|
y: py,
|
||||||
|
duration: 200,
|
||||||
|
ease: "Power2",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDespawn(obj: Phaser.GameObjects.Container): void {
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: obj,
|
||||||
|
alpha: 0,
|
||||||
|
scale: 0.5,
|
||||||
|
duration: 200,
|
||||||
|
ease: "Back.easeIn",
|
||||||
|
onComplete: () => obj.destroy(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getItemColor(itemId: string): number {
|
||||||
|
const hash = itemId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||||
|
return ITEM_COLORS[hash % ITEM_COLORS.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInventoryItemSpawner(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
inventorySignal: InventorySignal,
|
||||||
|
gridOffsetX: number,
|
||||||
|
gridOffsetY: number,
|
||||||
|
) {
|
||||||
|
return spawnEffect(
|
||||||
|
new InventoryItemSpawner(scene, inventorySignal, gridOffsetX, gridOffsetY),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,13 @@
|
||||||
import { ReactiveScene } from "boardgame-phaser";
|
import { ReactiveScene } from "boardgame-phaser";
|
||||||
import { createButton } from "@/utils/createButton";
|
import { createButton } from "@/utils/createButton";
|
||||||
import { GRID_CONFIG, ITEM_COLORS } from "@/config";
|
import { GRID_CONFIG } from "@/config";
|
||||||
import { createInventorySignal } from "@/state/inventory";
|
import { createInventorySignal } from "@/state/inventory";
|
||||||
import {
|
import { createItemIn, data } from "boardgame-core/samples/slay-the-spire-like";
|
||||||
createItemIn,
|
import { createInventoryItemSpawner } from "./InventoryItemSpawner";
|
||||||
data,
|
|
||||||
type GameItemMeta,
|
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
|
||||||
import type { InventorySignal } from "@/state/inventory";
|
|
||||||
import type { InventoryItem } from "boardgame-core/samples/slay-the-spire-like";
|
|
||||||
import { SceneKey } from "./types";
|
import { SceneKey } from "./types";
|
||||||
|
|
||||||
export class InventoryTestScene extends ReactiveScene {
|
export class InventoryTestScene extends ReactiveScene {
|
||||||
private inventorySignal!: InventorySignal;
|
private inventorySignal = createInventorySignal();
|
||||||
private gridOffsetX = 0;
|
private gridOffsetX = 0;
|
||||||
private gridOffsetY = 0;
|
private gridOffsetY = 0;
|
||||||
|
|
||||||
|
|
@ -23,59 +18,62 @@ export class InventoryTestScene extends ReactiveScene {
|
||||||
create(): void {
|
create(): void {
|
||||||
super.create();
|
super.create();
|
||||||
|
|
||||||
this.inventorySignal = createInventorySignal();
|
|
||||||
|
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
this.gridOffsetX =
|
const inventory = this.inventorySignal.value;
|
||||||
(width - GRID_CONFIG.WIDTH * GRID_CONFIG.VIEWER_CELL_SIZE) / 2;
|
const invWidth = inventory.width;
|
||||||
this.gridOffsetY =
|
const invHeight = inventory.height;
|
||||||
(height - GRID_CONFIG.HEIGHT * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40;
|
|
||||||
|
|
||||||
this.drawGrid();
|
this.gridOffsetX = (width - invWidth * GRID_CONFIG.VIEWER_CELL_SIZE) / 2;
|
||||||
this.drawItems();
|
this.gridOffsetY =
|
||||||
|
(height - invHeight * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40;
|
||||||
|
|
||||||
|
this.drawGrid(invWidth, invHeight);
|
||||||
|
this.setupItemSpawner();
|
||||||
|
|
||||||
this.add
|
this.add
|
||||||
.text(width / 2, 30, "Inventory Signal Test (4x6)", {
|
.text(
|
||||||
fontSize: "24px",
|
width / 2,
|
||||||
color: "#ffffff",
|
30,
|
||||||
fontStyle: "bold",
|
"Inventory Signal Test (4x6)",
|
||||||
})
|
GRID_CONFIG.TITLE_STYLE,
|
||||||
|
)
|
||||||
.setOrigin(0.5);
|
.setOrigin(0.5);
|
||||||
|
|
||||||
this.createControls();
|
this.createControls();
|
||||||
|
|
||||||
this.add
|
this.add
|
||||||
.text(width / 2, height - 40, "Items update reactively via signals", {
|
.text(
|
||||||
fontSize: "14px",
|
width / 2,
|
||||||
color: "#aaaaaa",
|
height - 40,
|
||||||
})
|
"Items update reactively via signals",
|
||||||
|
GRID_CONFIG.SUBTITLE_STYLE,
|
||||||
|
)
|
||||||
.setOrigin(0.5);
|
.setOrigin(0.5);
|
||||||
|
|
||||||
// React to inventory changes by re-rendering
|
|
||||||
this.addEffect(() => {
|
|
||||||
const inventory = this.inventorySignal.value;
|
|
||||||
// Track item count for reactivity
|
|
||||||
const itemCount = inventory.items.size;
|
|
||||||
return () => {};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawGrid(): void {
|
private drawGrid(invWidth: number, invHeight: number): void {
|
||||||
const graphics = this.add.graphics();
|
const graphics = this.add.graphics();
|
||||||
|
|
||||||
for (let y = 0; y < GRID_CONFIG.HEIGHT; y++) {
|
for (let y = 0; y < invHeight; y++) {
|
||||||
for (let x = 0; x < GRID_CONFIG.WIDTH; x++) {
|
for (let x = 0; x < invWidth; x++) {
|
||||||
const px = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
const px = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||||
|
|
||||||
graphics.fillStyle(0x222233);
|
const isOccupied = this.inventorySignal.value.occupiedCells.has(
|
||||||
|
`${x},${y}`,
|
||||||
|
);
|
||||||
|
graphics.fillStyle(
|
||||||
|
isOccupied
|
||||||
|
? GRID_CONFIG.CELL_OCCUPIED_COLOR
|
||||||
|
: GRID_CONFIG.CELL_EMPTY_COLOR,
|
||||||
|
);
|
||||||
graphics.fillRect(
|
graphics.fillRect(
|
||||||
px + 1,
|
px + 1,
|
||||||
py + 1,
|
py + 1,
|
||||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||||
);
|
);
|
||||||
graphics.lineStyle(1, 0x555577);
|
graphics.lineStyle(1, GRID_CONFIG.GRID_LINE_COLOR);
|
||||||
graphics.strokeRect(
|
graphics.strokeRect(
|
||||||
px,
|
px,
|
||||||
py,
|
py,
|
||||||
|
|
@ -86,68 +84,14 @@ export class InventoryTestScene extends ReactiveScene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawItems(): void {
|
private setupItemSpawner(): void {
|
||||||
const inventory = this.inventorySignal.value;
|
const spawner = createInventoryItemSpawner(
|
||||||
for (const [itemId, item] of inventory.items) {
|
this,
|
||||||
this.drawItem(itemId, item);
|
this.inventorySignal,
|
||||||
}
|
this.gridOffsetX,
|
||||||
}
|
this.gridOffsetY,
|
||||||
|
);
|
||||||
private drawItem(itemId: string, item: InventoryItem<GameItemMeta>): void {
|
this.disposables.add(spawner);
|
||||||
const shape = item.shape;
|
|
||||||
const transform = item.transform;
|
|
||||||
|
|
||||||
const cells: { x: number; y: number }[] = [];
|
|
||||||
for (let y = 0; y < shape.height; y++) {
|
|
||||||
for (let x = 0; x < shape.width; x++) {
|
|
||||||
if (shape.grid[y]?.[x]) {
|
|
||||||
const finalX = x + transform.offset.x;
|
|
||||||
const finalY = y + transform.offset.y;
|
|
||||||
cells.push({ x: finalX, y: finalY });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemColor = this.getItemColor(itemId);
|
|
||||||
const graphics = this.add.graphics();
|
|
||||||
|
|
||||||
for (const cell of cells) {
|
|
||||||
const px = this.gridOffsetX + cell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
|
||||||
const py = this.gridOffsetY + cell.y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
|
||||||
|
|
||||||
graphics.fillStyle(itemColor);
|
|
||||||
graphics.fillRect(
|
|
||||||
px + 2,
|
|
||||||
py + 2,
|
|
||||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
|
||||||
GRID_CONFIG.VIEWER_CELL_SIZE - 4,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cells.length > 0) {
|
|
||||||
const firstCell = cells[0];
|
|
||||||
const px = this.gridOffsetX + firstCell.x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
|
||||||
const py = this.gridOffsetY + firstCell.y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
|
||||||
const itemName = item.meta?.itemData.name ?? item.id;
|
|
||||||
|
|
||||||
this.add
|
|
||||||
.text(
|
|
||||||
px + GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
|
||||||
py + GRID_CONFIG.VIEWER_CELL_SIZE / 2,
|
|
||||||
itemName,
|
|
||||||
{
|
|
||||||
fontSize: "11px",
|
|
||||||
color: "#ffffff",
|
|
||||||
fontStyle: "bold",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.setOrigin(0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 createControls(): void {
|
private createControls(): void {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { MapViewerScene } from "@/scenes/MapViewerScene";
|
||||||
import { GridViewerScene } from "@/scenes/GridViewerScene";
|
import { GridViewerScene } from "@/scenes/GridViewerScene";
|
||||||
import { ShapeViewerScene } from "@/scenes/ShapeViewerScene";
|
import { ShapeViewerScene } from "@/scenes/ShapeViewerScene";
|
||||||
import { GAME_CONFIG } from "@/config";
|
import { GAME_CONFIG } from "@/config";
|
||||||
|
import { InventoryTestScene } from "@/scenes/InventoryTestScene";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -11,6 +12,7 @@ export default function App() {
|
||||||
<div className="flex-1 flex relative justify-center items-center">
|
<div className="flex-1 flex relative justify-center items-center">
|
||||||
<PhaserGame initialScene="IndexScene" config={GAME_CONFIG}>
|
<PhaserGame initialScene="IndexScene" config={GAME_CONFIG}>
|
||||||
<PhaserScene scene={IndexScene} />
|
<PhaserScene scene={IndexScene} />
|
||||||
|
<PhaserScene scene={InventoryTestScene} />
|
||||||
<PhaserScene scene={MapViewerScene} />
|
<PhaserScene scene={MapViewerScene} />
|
||||||
<PhaserScene scene={GridViewerScene} />
|
<PhaserScene scene={GridViewerScene} />
|
||||||
<PhaserScene scene={ShapeViewerScene} />
|
<PhaserScene scene={ShapeViewerScene} />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue