Compare commits

..

3 Commits

Author SHA1 Message Date
hypercross 706de4e33a fix: inventory preview not actually there 2026-04-14 15:48:47 +08:00
hypercross 5342f1c09d feat: inventory preview? 2026-04-14 15:22:22 +08:00
hypercross 3ca2a16e29 refactor: global state for scenes 2026-04-14 14:21:51 +08:00
6 changed files with 296 additions and 208 deletions

View File

@ -7,6 +7,7 @@ type CleanupFn = void | (() => void);
// 前向声明,避免循环导入
export interface SceneController {
launch(sceneKey: string): Promise<void>;
restart(): Promise<void>;
currentScene: ReadonlySignal<string | null>;
isTransitioning: ReadonlySignal<boolean>;
}

View File

@ -1,18 +1,16 @@
import Phaser from 'phaser';
import { ReactiveScene } from 'boardgame-phaser';
import { MutableSignal } from 'boardgame-core';
import {
createRunState,
canMoveTo,
moveToNode,
getCurrentNode,
getReachableChildren,
isAtEndNode,
isAtStartNode,
type RunState,
type MapNode,
type EncounterResult,
type EncounterState,
} from 'boardgame-core/samples/slay-the-spire-like';
import { PlaceholderEncounterScene, type EncounterData } from './PlaceholderEncounterScene';
const NODE_COLORS: Record<string, number> = {
start: 0x44aa44,
@ -37,8 +35,8 @@ const NODE_LABELS: Record<string, string> = {
};
export class GameFlowScene extends ReactiveScene {
private runState: RunState;
private seed: number;
/** 全局游戏状态(由 App.tsx 注入) */
private gameState: MutableSignal<RunState>;
// Layout constants
private readonly LAYER_HEIGHT = 110;
@ -63,10 +61,9 @@ export class GameFlowScene extends ReactiveScene {
private hoveredNode: string | null = null;
private nodeGraphics: Map<string, Phaser.GameObjects.Graphics> = new Map();
constructor() {
constructor(gameState: MutableSignal<RunState>) {
super('GameFlowScene');
this.seed = Date.now();
this.runState = createRunState(this.seed);
this.gameState = gameState;
}
create(): void {
@ -114,7 +111,8 @@ export class GameFlowScene extends ReactiveScene {
}
private updateHUD(): void {
const { player, currentNodeId, map } = this.runState;
const state = this.gameState.value;
const { player, currentNodeId, map } = state;
const currentNode = map.nodes.get(currentNodeId);
this.hpText.setText(`HP: ${player.currentHp}/${player.maxHp}`);
@ -129,12 +127,13 @@ export class GameFlowScene extends ReactiveScene {
private drawMap(): void {
const { width, height } = this.scale;
const state = this.gameState.value;
// Calculate map bounds
// Calculate map bounds (left-to-right: layers along X, nodes along Y)
const maxLayer = 9;
const maxNodesInLayer = 5;
const mapWidth = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
const mapHeight = maxLayer * this.LAYER_HEIGHT + 200;
const mapWidth = maxLayer * this.LAYER_HEIGHT + 200;
const mapHeight = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
// Create scrollable container
this.mapContainer = this.add.container(width / 2, height / 2 + 50);
@ -146,8 +145,8 @@ export class GameFlowScene extends ReactiveScene {
const graphics = this.add.graphics();
this.mapContainer.add(graphics);
const { map, currentNodeId } = this.runState;
const reachableChildren = getReachableChildren(this.runState);
const { map, currentNodeId } = state;
const reachableChildren = getReachableChildren(state);
const reachableIds = new Set(reachableChildren.map(n => n.id));
// Draw edges
@ -212,10 +211,11 @@ export class GameFlowScene extends ReactiveScene {
);
}
// Make reachable nodes interactive
// Make reachable nodes interactive (add hitZone to mapContainer so positions match)
if (isReachable) {
const hitZone = this.add.circle(posX, posY, this.NODE_RADIUS, 0x000000, 0)
.setInteractive({ useHandCursor: true });
this.mapContainer.add(hitZone);
hitZone.on('pointerover', () => {
this.hoveredNode = nodeId;
@ -272,12 +272,13 @@ export class GameFlowScene extends ReactiveScene {
}
private async onNodeClick(nodeId: string): Promise<void> {
if (!canMoveTo(this.runState, nodeId)) {
const state = this.gameState.value;
if (!canMoveTo(state, nodeId)) {
return;
}
// Move to target node
const result = moveToNode(this.runState, nodeId);
const result = moveToNode(state, nodeId);
if (!result.success) {
console.warn(`无法移动到节点: ${result.reason}`);
return;
@ -288,50 +289,25 @@ export class GameFlowScene extends ReactiveScene {
this.redrawMapHighlights();
// Check if at end node
if (isAtEndNode(this.runState)) {
if (isAtEndNode(state)) {
this.showEndScreen();
return;
}
// Launch encounter scene
const currentNode = getCurrentNode(this.runState);
const currentNode = getCurrentNode(state);
if (!currentNode || !currentNode.encounter) {
console.warn('当前节点没有遭遇数据');
return;
}
// Create encounter data
const encounterData: EncounterData = {
runState: this.runState,
nodeId: currentNode.id,
encounter: {
type: currentNode.type,
name: currentNode.encounter.name,
description: currentNode.encounter.description,
},
onComplete: (result: EncounterResult) => {
// Encounter completed, update HUD
this.updateHUD();
this.redrawMapHighlights();
},
};
// Re-add encounter scene with new data
const phaserGame = this.phaserGame.value.game;
const encounterScene = new PlaceholderEncounterScene();
if (!phaserGame.scene.getScene('PlaceholderEncounterScene')) {
phaserGame.scene.add('PlaceholderEncounterScene', encounterScene, false, {
...encounterData,
phaserGame: this.phaserGame,
sceneController: this.sceneController,
});
}
await this.sceneController.launch('PlaceholderEncounterScene');
}
private redrawMapHighlights(): void {
const { map, currentNodeId } = this.runState;
const reachableChildren = getReachableChildren(this.runState);
const state = this.gameState.value;
const { map, currentNodeId } = state;
const reachableChildren = getReachableChildren(state);
const reachableIds = new Set(reachableChildren.map(n => n.id));
for (const [nodeId, nodeGraphics] of this.nodeGraphics) {
@ -360,6 +336,7 @@ export class GameFlowScene extends ReactiveScene {
private showEndScreen(): void {
const { width, height } = this.scale;
const state = this.gameState.value;
// Overlay
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(300);
@ -371,7 +348,7 @@ export class GameFlowScene extends ReactiveScene {
fontStyle: 'bold',
}).setOrigin(0.5).setDepth(300);
const { player } = this.runState;
const { player } = state;
this.add.text(width / 2, height / 2 + 20, `剩余 HP: ${player.currentHp}/${player.maxHp}\n剩余金币: ${player.gold}`, {
fontSize: '20px',
color: '#ffffff',
@ -384,15 +361,18 @@ export class GameFlowScene extends ReactiveScene {
}
private getNodeX(node: MapNode): number {
const layer = this.runState.map.layers[node.layerIndex];
const nodeIndex = layer.nodeIds.indexOf(node.id);
const totalNodes = layer.nodeIds.length;
const layerWidth = (totalNodes - 1) * this.NODE_SPACING;
return -layerWidth / 2 + nodeIndex * this.NODE_SPACING;
// Layers go left-to-right along X axis
return -500 + node.layerIndex * this.LAYER_HEIGHT;
}
private getNodeY(node: MapNode): number {
return -600 + node.layerIndex * this.LAYER_HEIGHT;
// Nodes within a layer are spread vertically along Y axis
const state = this.gameState.value;
const layer = state.map.layers[node.layerIndex];
const nodeIndex = layer.nodeIds.indexOf(node.id);
const totalNodes = layer.nodeIds.length;
const layerHeight = (totalNodes - 1) * this.NODE_SPACING;
return -layerHeight / 2 + nodeIndex * this.NODE_SPACING;
}
private brightenColor(color: number): number {

View File

@ -116,8 +116,8 @@ export class MapViewerScene extends ReactiveScene {
});
// Legend (bottom-left, fixed)
this.legendContainer = this.add.container(20, this.scale.height - 200).setDepth(100);
const legendBg = this.add.rectangle(75, 90, 150, 180, 0x222222, 0.8);
this.legendContainer = this.add.container(20, this.scale.height - 180).setDepth(100);
const legendBg = this.add.rectangle(75, 80, 150, 160, 0x222222, 0.8);
this.legendContainer.add(legendBg);
this.legendContainer.add(
@ -132,11 +132,11 @@ export class MapViewerScene extends ReactiveScene {
this.legendContainer.add(
this.add.text(40, offsetY - 5, NODE_LABELS[type as MapNodeType], { fontSize: '12px', color: '#ffffff' })
);
offsetY += 22;
offsetY += 20;
}
// Hint text
this.add.text(width / 2, this.scale.height - 20, '拖拽滚动查看地图', {
this.add.text(width / 2, this.scale.height - 20, '拖拽滚动查看地图 (从左到右)', {
fontSize: '14px',
color: '#888888',
}).setOrigin(0.5).setDepth(100);
@ -150,11 +150,11 @@ export class MapViewerScene extends ReactiveScene {
// Update title
this.titleText.setText(`Map Viewer (Seed: ${this.seed})`);
// Calculate map bounds
// Calculate map bounds (left-to-right: layers along X, nodes along Y)
const maxLayer = 9; // TOTAL_LAYERS - 1 (10 layers: 0-9)
const maxNodesInLayer = 5; // widest layer (settlement has 4 nodes)
const mapWidth = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
const mapHeight = maxLayer * this.LAYER_HEIGHT + 200;
const mapWidth = maxLayer * this.LAYER_HEIGHT + 200;
const mapHeight = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
// Create scrollable container
this.mapContainer = this.add.container(width / 2, height / 2);
@ -240,14 +240,16 @@ export class MapViewerScene extends ReactiveScene {
}
private getNodeX(node: MapNode): number {
const layer = this.map!.layers[node.layerIndex];
const nodeIndex = layer.nodeIds.indexOf(node.id);
const totalNodes = layer.nodeIds.length;
const layerWidth = (totalNodes - 1) * this.NODE_SPACING;
return -layerWidth / 2 + nodeIndex * this.NODE_SPACING;
// Layers go left-to-right along X axis
return -500 + node.layerIndex * this.LAYER_HEIGHT;
}
private getNodeY(node: MapNode): number {
return -600 + node.layerIndex * this.LAYER_HEIGHT;
// Nodes within a layer are spread vertically along Y axis
const layer = this.map!.layers[node.layerIndex];
const nodeIndex = layer.nodeIds.indexOf(node.id);
const totalNodes = layer.nodeIds.length;
const layerHeight = (totalNodes - 1) * this.NODE_SPACING;
return -layerHeight / 2 + nodeIndex * this.NODE_SPACING;
}
}

View File

@ -1,187 +1,259 @@
import Phaser from 'phaser';
import { ReactiveScene } from 'boardgame-phaser';
import { MutableSignal } from 'boardgame-core';
import {
resolveEncounter,
type RunState,
type EncounterResult,
type MapNodeType,
type InventoryItem,
type GameItemMeta,
} from 'boardgame-core/samples/slay-the-spire-like';
/** 遭遇场景接收的数据 */
export interface EncounterData {
runState: RunState;
nodeId: string;
encounter: { type: MapNodeType; name: string; description: string };
onComplete: (result: EncounterResult) => void;
}
/**
*
*
* "完成遭遇"
*
* encounter.type
* - MapNodeType.Minion / Elite CombatEncounterScene
* - MapNodeType.Shop ShopEncounterScene
* - MapNodeType.Camp CampEncounterScene
* - MapNodeType.Event EventEncounterScene
* - MapNodeType.Curio CurioEncounterScene
*
* 80x80
*/
export class PlaceholderEncounterScene extends ReactiveScene<EncounterData> {
constructor() {
export class PlaceholderEncounterScene extends ReactiveScene {
private gameState: MutableSignal<RunState>;
// Grid constants
private readonly CELL_SIZE = 80;
private readonly GRID_GAP = 2;
private gridX = 0;
private gridY = 0;
constructor(gameState: MutableSignal<RunState>) {
super('PlaceholderEncounterScene');
this.gameState = gameState;
}
create(): void {
super.create();
const { width, height } = this.scale;
const centerX = width / 2;
const centerY = height / 2;
const { encounter, nodeId } = this.initData;
const state = this.gameState.value;
// ── Layout: split screen into left (grid) and right (encounter) ──
const gridCols = state.inventory.width; // 6
const gridRows = state.inventory.height; // 4
const gridW = gridCols * this.CELL_SIZE;
const gridH = gridRows * this.CELL_SIZE;
const leftPanelW = gridW + 40; // panel padding
this.gridX = 60;
this.gridY = (height - gridH) / 2 + 20;
// Ensure camera shows the full grid
this.cameras.main.setBounds(0, 0, width, height);
this.cameras.main.setScroll(0, 0);
// ── LEFT PANEL: inventory grid ──
this.drawLeftPanel(leftPanelW, gridW, gridH);
// ── RIGHT PANEL: encounter info ──
const node = state.map.nodes.get(state.currentNodeId);
if (!node || !node.encounter) {
const rightX = leftPanelW + 80;
this.add.text(rightX + 300, height / 2, '没有遭遇数据', {
fontSize: '24px', color: '#ff4444',
}).setOrigin(0.5);
return;
}
this.drawRightPanel(node, leftPanelW, width, height);
}
// ───────────────────── LEFT PANEL ─────────────────────
private drawLeftPanel(panelW: number, gridW: number, gridH: number): void {
// Panel background
this.add.rectangle(
this.gridX + panelW / 2, this.gridY + gridH / 2,
panelW + 10, gridH + 50,
0x111122, 0.9
).setStrokeStyle(2, 0x5555aa);
// "背包" title
this.add.text(this.gridX + gridW / 2, this.gridY - 20, '背包', {
fontSize: '22px', color: '#ffffff', fontStyle: 'bold',
}).setOrigin(0.5);
const graphics = this.add.graphics();
// Draw empty cell backgrounds
for (let y = 0; y < 4; y++) {
for (let x = 0; x < 6; x++) {
const px = this.gridX + x * this.CELL_SIZE;
const py = this.gridY + y * this.CELL_SIZE;
// Dark cell fill
graphics.fillStyle(0x1a1a2e);
graphics.fillRect(px + 1, py + 1, this.CELL_SIZE - 2, this.CELL_SIZE - 2);
// Cell border
graphics.lineStyle(2, 0x444477);
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
}
}
// Draw items on top
this.drawItems(graphics);
}
private drawItems(graphics: Phaser.GameObjects.Graphics): void {
const state = this.gameState.value;
const palette = [0x3388ff, 0xff8833, 0x33ff88, 0xff3388, 0x8833ff, 0x33ffff, 0xffff33, 0xff6633];
const colorMap = new Map<string, number>();
let idx = 0;
for (const [id, item] of state.inventory.items) {
const color = colorMap.get(id) ?? palette[idx++ % palette.length];
colorMap.set(id, color);
const cells = this.getItemCells(item);
if (cells.length === 0) continue;
// Filled cells
for (const c of cells) {
const px = this.gridX + c.x * this.CELL_SIZE;
const py = this.gridY + c.y * this.CELL_SIZE;
graphics.fillStyle(color);
graphics.fillRect(px + 2, py + 2, this.CELL_SIZE - 4, this.CELL_SIZE - 4);
graphics.lineStyle(2, 0xffffff);
graphics.strokeRect(px, py, this.CELL_SIZE, this.CELL_SIZE);
}
// Item name
const first = cells[0];
const name = item.meta?.itemData?.name ?? item.id;
this.add.text(
this.gridX + first.x * this.CELL_SIZE + this.CELL_SIZE / 2,
this.gridY + first.y * this.CELL_SIZE + this.CELL_SIZE / 2,
name, { fontSize: '12px', color: '#fff', fontStyle: 'bold' }
).setOrigin(0.5);
}
}
// ───────────────────── RIGHT PANEL ─────────────────────
private drawRightPanel(node: any, leftPanelW: number, width: number, height: number): void {
const encounter = {
type: node.type as MapNodeType,
name: node.encounter.name as string,
description: node.encounter.description as string,
};
const nodeId = node.id as string;
const rightX = leftPanelW + 60;
const rightW = width - rightX - 40;
const cx = rightX + rightW / 2;
const cy = height / 2;
// Title
this.add.text(centerX, centerY - 150, '遭遇', {
fontSize: '32px',
color: '#ffffff',
fontStyle: 'bold',
this.add.text(cx, cy - 180, '遭遇', {
fontSize: '36px', color: '#fff', fontStyle: 'bold',
}).setOrigin(0.5);
// Encounter type badge
const typeLabel = this.getEncounterTypeLabel(encounter.type);
const typeBg = this.add.rectangle(centerX, centerY - 80, 120, 36, this.getEncounterTypeColor(encounter.type));
this.add.text(centerX, centerY - 80, typeLabel, {
fontSize: '16px',
color: '#ffffff',
fontStyle: 'bold',
// Type badge
const typeLabel = this.getTypeLabel(encounter.type);
const badgeColor = this.getTypeColor(encounter.type);
this.add.rectangle(cx, cy - 110, 140, 40, badgeColor);
this.add.text(cx, cy - 110, typeLabel, {
fontSize: '18px', color: '#fff', fontStyle: 'bold',
}).setOrigin(0.5);
// Encounter name
this.add.text(centerX, centerY - 30, encounter.name, {
fontSize: '24px',
color: '#ffffff',
// Name
this.add.text(cx, cy - 50, encounter.name, {
fontSize: '28px', color: '#fff',
}).setOrigin(0.5);
// Encounter description
this.add.text(centerX, centerY + 20, encounter.description || '(暂无描述)', {
fontSize: '16px',
color: '#aaaaaa',
wordWrap: { width: 600 },
align: 'center',
// Description
this.add.text(cx, cy + 10, encounter.description || '(暂无描述)', {
fontSize: '18px', color: '#bbb',
wordWrap: { width: rightW - 40 }, align: 'center',
}).setOrigin(0.5);
// Node ID info
this.add.text(centerX, centerY + 80, `节点: ${nodeId}`, {
fontSize: '12px',
color: '#666666',
// Node id
this.add.text(cx, cy + 80, `节点: ${nodeId}`, {
fontSize: '14px', color: '#666',
}).setOrigin(0.5);
// Placeholder notice
this.add.text(centerX, centerY + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', {
fontSize: '14px',
color: '#ff8844',
fontStyle: 'italic',
this.add.text(cx, cy + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', {
fontSize: '14px', color: '#ff8844', fontStyle: 'italic',
}).setOrigin(0.5);
// Complete button
this.createButton('完成遭遇', centerX, centerY + 200, 200, 50, async () => {
// Buttons
this.createButton('完成遭遇', cx, cy + 200, 220, 50, async () => {
await this.completeEncounter();
});
// Back button (without resolving)
this.createButton('暂不处理', centerX, centerY + 270, 200, 40, async () => {
this.createButton('暂不处理', cx, cy + 270, 220, 40, async () => {
await this.sceneController.launch('GameFlowScene');
});
}
// ───────────────────── Helpers ─────────────────────
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 getTypeLabel(type: MapNodeType): string {
const m: Record<MapNodeType, string> = {
start: '起点', end: '终点', minion: '战斗', elite: '精英战斗',
event: '事件', camp: '营地', shop: '商店', curio: '奇遇',
};
return m[type] ?? type;
}
private getTypeColor(type: MapNodeType): number {
const m: Record<MapNodeType, number> = {
start: 0x44aa44, end: 0xcc8844, minion: 0xcc4444, elite: 0xcc44cc,
event: 0xaaaa44, camp: 0x44cccc, shop: 0x4488cc, curio: 0x8844cc,
};
return m[type] ?? 0x888888;
}
private createButton(label: string, x: number, y: number, w: number, h: number, onClick: () => void): void {
const bg = this.add.rectangle(x, y, w, h, 0x444466)
.setStrokeStyle(2, 0x7777aa).setInteractive({ useHandCursor: true });
const txt = this.add.text(x, y, label, { fontSize: '16px', color: '#fff' }).setOrigin(0.5);
bg.on('pointerover', () => { bg.setFillStyle(0x555588); txt.setScale(1.05); });
bg.on('pointerout', () => { bg.setFillStyle(0x444466); txt.setScale(1); });
bg.on('pointerdown', onClick);
}
private async completeEncounter(): Promise<void> {
const { runState, nodeId, encounter, onComplete } = this.initData;
const state = this.gameState.value;
const node = state.map.nodes.get(state.currentNodeId);
if (!node || !node.encounter) return;
// 生成模拟遭遇结果
const result: EncounterResult = this.generatePlaceholderResult(encounter.type);
// 调用进度管理器结算遭遇
resolveEncounter(runState, result);
// 回调通知上层
onComplete(result);
// 返回游戏流程场景
const result: EncounterResult = this.generatePlaceholderResult(node.type);
resolveEncounter(state, result);
await this.sceneController.launch('GameFlowScene');
}
private generatePlaceholderResult(type: MapNodeType): EncounterResult {
// 根据遭遇类型生成不同的模拟结果
switch (type) {
case 'minion':
case 'elite':
return { hpLost: type === 'elite' ? 15 : 8, goldEarned: type === 'elite' ? 30 : 15 };
case 'camp':
return { hpGained: 15 };
case 'shop':
return { goldEarned: 0 };
case 'minion': return { hpLost: 8, goldEarned: 15 };
case 'elite': return { hpLost: 15, goldEarned: 30 };
case 'camp': return { hpGained: 15 };
case 'shop': return { goldEarned: 0 };
case 'curio':
case 'event':
return { goldEarned: 20 };
default:
return {};
case 'event': return { goldEarned: 20 };
default: return {};
}
}
private getEncounterTypeLabel(type: MapNodeType): string {
const labels: Record<MapNodeType, string> = {
start: '起点',
end: '终点',
minion: '战斗',
elite: '精英战斗',
event: '事件',
camp: '营地',
shop: '商店',
curio: '奇遇',
};
return labels[type] ?? type;
}
private getEncounterTypeColor(type: MapNodeType): number {
const colors: Record<MapNodeType, number> = {
start: 0x44aa44,
end: 0xcc8844,
minion: 0xcc4444,
elite: 0xcc44cc,
event: 0xaaaa44,
camp: 0x44cccc,
shop: 0x4488cc,
curio: 0x8844cc,
};
return colors[type] ?? 0x888888;
}
private createButton(
label: string,
x: number,
y: number,
width: number,
height: number,
onClick: () => void
): void {
const bg = this.add.rectangle(x, y, width, height, 0x444466)
.setStrokeStyle(2, 0x7777aa)
.setInteractive({ useHandCursor: true });
const text = this.add.text(x, y, label, {
fontSize: '16px',
color: '#ffffff',
}).setOrigin(0.5);
bg.on('pointerover', () => {
bg.setFillStyle(0x555588);
text.setScale(1.05);
});
bg.on('pointerout', () => {
bg.setFillStyle(0x444466);
text.setScale(1);
});
bg.on('pointerdown', onClick);
}
}

View File

@ -0,0 +1,29 @@
import { MutableSignal, mutableSignal } from 'boardgame-core';
import { createRunState, type RunState } from 'boardgame-core/samples/slay-the-spire-like';
/**
* Signal
*
* App.tsx
* signal UI
*/
export function createGameState(seed?: number): MutableSignal<RunState> {
return mutableSignal<RunState>(createRunState(seed));
}
/** 获取当前遭遇数据computed getter */
export function currentEncounter(
gameState: MutableSignal<RunState>
): { nodeId: string; encounter: { name: string; description: string; type: string } } | null {
const state = gameState.value;
const node = state.map.nodes.get(state.currentNodeId);
if (!node || !node.encounter) return null;
return {
nodeId: node.id,
encounter: {
type: node.type,
name: node.encounter.name,
description: node.encounter.description,
},
};
}

View File

@ -7,14 +7,18 @@ 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();
export default function App() {
const indexScene = useMemo(() => new IndexScene(), []);
const mapViewerScene = useMemo(() => new MapViewerScene(), []);
const gridViewerScene = useMemo(() => new GridViewerScene(), []);
const shapeViewerScene = useMemo(() => new ShapeViewerScene(), []);
const gameFlowScene = useMemo(() => new GameFlowScene(), []);
const placeholderEncounterScene = useMemo(() => new PlaceholderEncounterScene(), []);
const gameFlowScene = useMemo(() => new GameFlowScene(gameState), []);
const placeholderEncounterScene = useMemo(() => new PlaceholderEncounterScene(gameState), []);
return (
<div className="flex flex-col h-screen">