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,
|
||||
/** Gap between grid cells (pixels) */
|
||||
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;
|
||||
|
||||
// ── 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 { createButton } from "@/utils/createButton";
|
||||
import { GRID_CONFIG, ITEM_COLORS } from "@/config";
|
||||
import { GRID_CONFIG } from "@/config";
|
||||
import { createInventorySignal } from "@/state/inventory";
|
||||
import {
|
||||
createItemIn,
|
||||
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 { createItemIn, data } from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { createInventoryItemSpawner } from "./InventoryItemSpawner";
|
||||
import { SceneKey } from "./types";
|
||||
|
||||
export class InventoryTestScene extends ReactiveScene {
|
||||
private inventorySignal!: InventorySignal;
|
||||
private inventorySignal = createInventorySignal();
|
||||
private gridOffsetX = 0;
|
||||
private gridOffsetY = 0;
|
||||
|
||||
|
|
@ -23,59 +18,62 @@ export class InventoryTestScene extends ReactiveScene {
|
|||
create(): void {
|
||||
super.create();
|
||||
|
||||
this.inventorySignal = createInventorySignal();
|
||||
|
||||
const { width, height } = this.scale;
|
||||
this.gridOffsetX =
|
||||
(width - GRID_CONFIG.WIDTH * GRID_CONFIG.VIEWER_CELL_SIZE) / 2;
|
||||
this.gridOffsetY =
|
||||
(height - GRID_CONFIG.HEIGHT * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40;
|
||||
const inventory = this.inventorySignal.value;
|
||||
const invWidth = inventory.width;
|
||||
const invHeight = inventory.height;
|
||||
|
||||
this.drawGrid();
|
||||
this.drawItems();
|
||||
this.gridOffsetX = (width - invWidth * GRID_CONFIG.VIEWER_CELL_SIZE) / 2;
|
||||
this.gridOffsetY =
|
||||
(height - invHeight * GRID_CONFIG.VIEWER_CELL_SIZE) / 2 + 40;
|
||||
|
||||
this.drawGrid(invWidth, invHeight);
|
||||
this.setupItemSpawner();
|
||||
|
||||
this.add
|
||||
.text(width / 2, 30, "Inventory Signal Test (4x6)", {
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.text(
|
||||
width / 2,
|
||||
30,
|
||||
"Inventory Signal Test (4x6)",
|
||||
GRID_CONFIG.TITLE_STYLE,
|
||||
)
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.createControls();
|
||||
|
||||
this.add
|
||||
.text(width / 2, height - 40, "Items update reactively via signals", {
|
||||
fontSize: "14px",
|
||||
color: "#aaaaaa",
|
||||
})
|
||||
.text(
|
||||
width / 2,
|
||||
height - 40,
|
||||
"Items update reactively via signals",
|
||||
GRID_CONFIG.SUBTITLE_STYLE,
|
||||
)
|
||||
.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();
|
||||
|
||||
for (let y = 0; y < GRID_CONFIG.HEIGHT; y++) {
|
||||
for (let x = 0; x < GRID_CONFIG.WIDTH; x++) {
|
||||
for (let y = 0; y < invHeight; y++) {
|
||||
for (let x = 0; x < invWidth; x++) {
|
||||
const px = this.gridOffsetX + x * 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(
|
||||
px + 1,
|
||||
py + 1,
|
||||
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(
|
||||
px,
|
||||
py,
|
||||
|
|
@ -86,68 +84,14 @@ export class InventoryTestScene extends ReactiveScene {
|
|||
}
|
||||
}
|
||||
|
||||
private drawItems(): void {
|
||||
const inventory = this.inventorySignal.value;
|
||||
for (const [itemId, item] of inventory.items) {
|
||||
this.drawItem(itemId, item);
|
||||
}
|
||||
}
|
||||
|
||||
private drawItem(itemId: string, item: InventoryItem<GameItemMeta>): void {
|
||||
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,
|
||||
private setupItemSpawner(): void {
|
||||
const spawner = createInventoryItemSpawner(
|
||||
this,
|
||||
this.inventorySignal,
|
||||
this.gridOffsetX,
|
||||
this.gridOffsetY,
|
||||
);
|
||||
}
|
||||
|
||||
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];
|
||||
this.disposables.add(spawner);
|
||||
}
|
||||
|
||||
private createControls(): void {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { MapViewerScene } from "@/scenes/MapViewerScene";
|
|||
import { GridViewerScene } from "@/scenes/GridViewerScene";
|
||||
import { ShapeViewerScene } from "@/scenes/ShapeViewerScene";
|
||||
import { GAME_CONFIG } from "@/config";
|
||||
import { InventoryTestScene } from "@/scenes/InventoryTestScene";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
|
|
@ -11,6 +12,7 @@ export default function App() {
|
|||
<div className="flex-1 flex relative justify-center items-center">
|
||||
<PhaserGame initialScene="IndexScene" config={GAME_CONFIG}>
|
||||
<PhaserScene scene={IndexScene} />
|
||||
<PhaserScene scene={InventoryTestScene} />
|
||||
<PhaserScene scene={MapViewerScene} />
|
||||
<PhaserScene scene={GridViewerScene} />
|
||||
<PhaserScene scene={ShapeViewerScene} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue