feat(sts-like-viewer): add inventory test scene
Implement `InventoryTestScene` to demonstrate reactive inventory management using signals. Includes a grid-based visualization of items and controls to add or remove items dynamically.
This commit is contained in:
parent
368d9942d2
commit
b06876ccc0
|
|
@ -49,6 +49,11 @@ export class IndexScene extends ReactiveScene {
|
|||
scene: SceneKey.ShapeViewerScene,
|
||||
y: centerY + 140,
|
||||
},
|
||||
{
|
||||
label: "Inventory Test",
|
||||
scene: SceneKey.InventoryTestScene,
|
||||
y: centerY + 210,
|
||||
},
|
||||
];
|
||||
|
||||
for (const btn of buttons) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
import { ReactiveScene } from "boardgame-phaser";
|
||||
import { createButton } from "@/utils/createButton";
|
||||
import { GRID_CONFIG, ITEM_COLORS } 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 { SceneKey } from "./types";
|
||||
|
||||
export class InventoryTestScene extends ReactiveScene {
|
||||
private inventorySignal!: InventorySignal;
|
||||
private gridOffsetX = 0;
|
||||
private gridOffsetY = 0;
|
||||
|
||||
constructor() {
|
||||
super("InventoryTestScene");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
this.drawGrid();
|
||||
this.drawItems();
|
||||
|
||||
this.add
|
||||
.text(width / 2, 30, "Inventory Signal Test (4x6)", {
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
fontStyle: "bold",
|
||||
})
|
||||
.setOrigin(0.5);
|
||||
|
||||
this.createControls();
|
||||
|
||||
this.add
|
||||
.text(width / 2, height - 40, "Items update reactively via signals", {
|
||||
fontSize: "14px",
|
||||
color: "#aaaaaa",
|
||||
})
|
||||
.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 {
|
||||
const graphics = this.add.graphics();
|
||||
|
||||
for (let y = 0; y < GRID_CONFIG.HEIGHT; y++) {
|
||||
for (let x = 0; x < GRID_CONFIG.WIDTH; x++) {
|
||||
const px = this.gridOffsetX + x * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
const py = this.gridOffsetY + y * GRID_CONFIG.VIEWER_CELL_SIZE;
|
||||
|
||||
graphics.fillStyle(0x222233);
|
||||
graphics.fillRect(
|
||||
px + 1,
|
||||
py + 1,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE - 2,
|
||||
);
|
||||
graphics.lineStyle(1, 0x555577);
|
||||
graphics.strokeRect(
|
||||
px,
|
||||
py,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE,
|
||||
GRID_CONFIG.VIEWER_CELL_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
const { width } = this.scale;
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "返回菜单",
|
||||
x: 100,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch(SceneKey.IndexScene);
|
||||
},
|
||||
});
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "添加道具",
|
||||
x: width - 300,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.addRandomItem();
|
||||
},
|
||||
});
|
||||
|
||||
createButton({
|
||||
scene: this,
|
||||
label: "移除最后一个",
|
||||
x: width - 150,
|
||||
y: 40,
|
||||
onClick: () => {
|
||||
this.removeLastItem();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private addRandomItem(): void {
|
||||
const items = data.desert.getItems();
|
||||
|
||||
this.inventorySignal.produce((inventory) => {
|
||||
const usedIndices = new Set<number>();
|
||||
|
||||
for (const item of inventory.items.values()) {
|
||||
const match = item.id.match(/^item-(\d+)-/);
|
||||
if (match) {
|
||||
usedIndices.add(parseInt(match[1], 10));
|
||||
}
|
||||
}
|
||||
|
||||
let availableIndex = 0;
|
||||
while (usedIndices.has(availableIndex) && availableIndex < items.length) {
|
||||
availableIndex++;
|
||||
}
|
||||
|
||||
if (availableIndex >= items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemData = items[availableIndex];
|
||||
const id = `item-${availableIndex}-${Date.now().toString(16).slice(-4)}`;
|
||||
createItemIn(inventory, id, itemData);
|
||||
});
|
||||
}
|
||||
|
||||
private removeLastItem(): void {
|
||||
this.inventorySignal.produce((inventory) => {
|
||||
const items = Array.from(inventory.items.entries());
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lastId] = items[items.length - 1];
|
||||
inventory.items.delete(lastId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
export enum SceneKey {
|
||||
GridViewerScene = "GridViewerScene",
|
||||
IndexScene = "IndexScene",
|
||||
InventoryTestScene = "InventoryTestScene",
|
||||
MapViewerScene = "MapViewerScene",
|
||||
ShapeViewerScene = "ShapeViewerScene",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { mutableSignal } from "boardgame-core";
|
||||
import {
|
||||
createGridInventory,
|
||||
createItemIn,
|
||||
data,
|
||||
GameItemMeta,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
|
||||
function genId() {
|
||||
return Math.random().toString(16).slice(-8);
|
||||
}
|
||||
|
||||
export type InventorySignal = ReturnType<typeof createInventorySignal>;
|
||||
|
||||
export function createInventorySignal() {
|
||||
const inventory = createGridInventory<GameItemMeta>(4, 6);
|
||||
|
||||
const startingItems = data.desert.getStartingItems();
|
||||
for (const data of startingItems) {
|
||||
createItemIn(inventory, `${data.id}-${genId()}`, data);
|
||||
}
|
||||
|
||||
return mutableSignal(inventory);
|
||||
}
|
||||
Loading…
Reference in New Issue