refactor: global state for scenes
This commit is contained in:
parent
7fb9edcbf0
commit
3ca2a16e29
|
|
@ -7,6 +7,7 @@ type CleanupFn = void | (() => void);
|
||||||
// 前向声明,避免循环导入
|
// 前向声明,避免循环导入
|
||||||
export interface SceneController {
|
export interface SceneController {
|
||||||
launch(sceneKey: string): Promise<void>;
|
launch(sceneKey: string): Promise<void>;
|
||||||
|
restart(): Promise<void>;
|
||||||
currentScene: ReadonlySignal<string | null>;
|
currentScene: ReadonlySignal<string | null>;
|
||||||
isTransitioning: ReadonlySignal<boolean>;
|
isTransitioning: ReadonlySignal<boolean>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { ReactiveScene } from 'boardgame-phaser';
|
import { ReactiveScene } from 'boardgame-phaser';
|
||||||
|
import { MutableSignal } from 'boardgame-core';
|
||||||
import {
|
import {
|
||||||
createRunState,
|
|
||||||
canMoveTo,
|
canMoveTo,
|
||||||
moveToNode,
|
moveToNode,
|
||||||
getCurrentNode,
|
getCurrentNode,
|
||||||
getReachableChildren,
|
getReachableChildren,
|
||||||
isAtEndNode,
|
isAtEndNode,
|
||||||
|
isAtStartNode,
|
||||||
type RunState,
|
type RunState,
|
||||||
type MapNode,
|
type MapNode,
|
||||||
type EncounterResult,
|
|
||||||
type EncounterState,
|
|
||||||
} from 'boardgame-core/samples/slay-the-spire-like';
|
} from 'boardgame-core/samples/slay-the-spire-like';
|
||||||
import { PlaceholderEncounterScene, type EncounterData } from './PlaceholderEncounterScene';
|
|
||||||
|
|
||||||
const NODE_COLORS: Record<string, number> = {
|
const NODE_COLORS: Record<string, number> = {
|
||||||
start: 0x44aa44,
|
start: 0x44aa44,
|
||||||
|
|
@ -37,8 +35,8 @@ const NODE_LABELS: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class GameFlowScene extends ReactiveScene {
|
export class GameFlowScene extends ReactiveScene {
|
||||||
private runState: RunState;
|
/** 全局游戏状态(由 App.tsx 注入) */
|
||||||
private seed: number;
|
private gameState: MutableSignal<RunState>;
|
||||||
|
|
||||||
// Layout constants
|
// Layout constants
|
||||||
private readonly LAYER_HEIGHT = 110;
|
private readonly LAYER_HEIGHT = 110;
|
||||||
|
|
@ -63,10 +61,9 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
private hoveredNode: string | null = null;
|
private hoveredNode: string | null = null;
|
||||||
private nodeGraphics: Map<string, Phaser.GameObjects.Graphics> = new Map();
|
private nodeGraphics: Map<string, Phaser.GameObjects.Graphics> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor(gameState: MutableSignal<RunState>) {
|
||||||
super('GameFlowScene');
|
super('GameFlowScene');
|
||||||
this.seed = Date.now();
|
this.gameState = gameState;
|
||||||
this.runState = createRunState(this.seed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
|
|
@ -114,7 +111,8 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateHUD(): void {
|
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);
|
const currentNode = map.nodes.get(currentNodeId);
|
||||||
|
|
||||||
this.hpText.setText(`HP: ${player.currentHp}/${player.maxHp}`);
|
this.hpText.setText(`HP: ${player.currentHp}/${player.maxHp}`);
|
||||||
|
|
@ -129,12 +127,13 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
|
|
||||||
private drawMap(): void {
|
private drawMap(): void {
|
||||||
const { width, height } = this.scale;
|
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 maxLayer = 9;
|
||||||
const maxNodesInLayer = 5;
|
const maxNodesInLayer = 5;
|
||||||
const mapWidth = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
|
const mapWidth = maxLayer * this.LAYER_HEIGHT + 200;
|
||||||
const mapHeight = maxLayer * this.LAYER_HEIGHT + 200;
|
const mapHeight = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
|
||||||
|
|
||||||
// Create scrollable container
|
// Create scrollable container
|
||||||
this.mapContainer = this.add.container(width / 2, height / 2 + 50);
|
this.mapContainer = this.add.container(width / 2, height / 2 + 50);
|
||||||
|
|
@ -146,8 +145,8 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
const graphics = this.add.graphics();
|
const graphics = this.add.graphics();
|
||||||
this.mapContainer.add(graphics);
|
this.mapContainer.add(graphics);
|
||||||
|
|
||||||
const { map, currentNodeId } = this.runState;
|
const { map, currentNodeId } = state;
|
||||||
const reachableChildren = getReachableChildren(this.runState);
|
const reachableChildren = getReachableChildren(state);
|
||||||
const reachableIds = new Set(reachableChildren.map(n => n.id));
|
const reachableIds = new Set(reachableChildren.map(n => n.id));
|
||||||
|
|
||||||
// Draw edges
|
// 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) {
|
if (isReachable) {
|
||||||
const hitZone = this.add.circle(posX, posY, this.NODE_RADIUS, 0x000000, 0)
|
const hitZone = this.add.circle(posX, posY, this.NODE_RADIUS, 0x000000, 0)
|
||||||
.setInteractive({ useHandCursor: true });
|
.setInteractive({ useHandCursor: true });
|
||||||
|
this.mapContainer.add(hitZone);
|
||||||
|
|
||||||
hitZone.on('pointerover', () => {
|
hitZone.on('pointerover', () => {
|
||||||
this.hoveredNode = nodeId;
|
this.hoveredNode = nodeId;
|
||||||
|
|
@ -272,12 +272,13 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onNodeClick(nodeId: string): Promise<void> {
|
private async onNodeClick(nodeId: string): Promise<void> {
|
||||||
if (!canMoveTo(this.runState, nodeId)) {
|
const state = this.gameState.value;
|
||||||
|
if (!canMoveTo(state, nodeId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to target node
|
// Move to target node
|
||||||
const result = moveToNode(this.runState, nodeId);
|
const result = moveToNode(state, nodeId);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.warn(`无法移动到节点: ${result.reason}`);
|
console.warn(`无法移动到节点: ${result.reason}`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -288,50 +289,25 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
this.redrawMapHighlights();
|
this.redrawMapHighlights();
|
||||||
|
|
||||||
// Check if at end node
|
// Check if at end node
|
||||||
if (isAtEndNode(this.runState)) {
|
if (isAtEndNode(state)) {
|
||||||
this.showEndScreen();
|
this.showEndScreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch encounter scene
|
// Launch encounter scene
|
||||||
const currentNode = getCurrentNode(this.runState);
|
const currentNode = getCurrentNode(state);
|
||||||
if (!currentNode || !currentNode.encounter) {
|
if (!currentNode || !currentNode.encounter) {
|
||||||
|
console.warn('当前节点没有遭遇数据');
|
||||||
return;
|
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');
|
await this.sceneController.launch('PlaceholderEncounterScene');
|
||||||
}
|
}
|
||||||
|
|
||||||
private redrawMapHighlights(): void {
|
private redrawMapHighlights(): void {
|
||||||
const { map, currentNodeId } = this.runState;
|
const state = this.gameState.value;
|
||||||
const reachableChildren = getReachableChildren(this.runState);
|
const { map, currentNodeId } = state;
|
||||||
|
const reachableChildren = getReachableChildren(state);
|
||||||
const reachableIds = new Set(reachableChildren.map(n => n.id));
|
const reachableIds = new Set(reachableChildren.map(n => n.id));
|
||||||
|
|
||||||
for (const [nodeId, nodeGraphics] of this.nodeGraphics) {
|
for (const [nodeId, nodeGraphics] of this.nodeGraphics) {
|
||||||
|
|
@ -360,6 +336,7 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
|
|
||||||
private showEndScreen(): void {
|
private showEndScreen(): void {
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
|
const state = this.gameState.value;
|
||||||
|
|
||||||
// Overlay
|
// Overlay
|
||||||
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(300);
|
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',
|
fontStyle: 'bold',
|
||||||
}).setOrigin(0.5).setDepth(300);
|
}).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}`, {
|
this.add.text(width / 2, height / 2 + 20, `剩余 HP: ${player.currentHp}/${player.maxHp}\n剩余金币: ${player.gold}`, {
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
|
|
@ -384,15 +361,18 @@ export class GameFlowScene extends ReactiveScene {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNodeX(node: MapNode): number {
|
private getNodeX(node: MapNode): number {
|
||||||
const layer = this.runState.map.layers[node.layerIndex];
|
// Layers go left-to-right along X axis
|
||||||
const nodeIndex = layer.nodeIds.indexOf(node.id);
|
return -500 + node.layerIndex * this.LAYER_HEIGHT;
|
||||||
const totalNodes = layer.nodeIds.length;
|
|
||||||
const layerWidth = (totalNodes - 1) * this.NODE_SPACING;
|
|
||||||
return -layerWidth / 2 + nodeIndex * this.NODE_SPACING;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNodeY(node: MapNode): number {
|
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 {
|
private brightenColor(color: number): number {
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,8 @@ export class MapViewerScene extends ReactiveScene {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Legend (bottom-left, fixed)
|
// Legend (bottom-left, fixed)
|
||||||
this.legendContainer = this.add.container(20, this.scale.height - 200).setDepth(100);
|
this.legendContainer = this.add.container(20, this.scale.height - 180).setDepth(100);
|
||||||
const legendBg = this.add.rectangle(75, 90, 150, 180, 0x222222, 0.8);
|
const legendBg = this.add.rectangle(75, 80, 150, 160, 0x222222, 0.8);
|
||||||
this.legendContainer.add(legendBg);
|
this.legendContainer.add(legendBg);
|
||||||
|
|
||||||
this.legendContainer.add(
|
this.legendContainer.add(
|
||||||
|
|
@ -132,11 +132,11 @@ export class MapViewerScene extends ReactiveScene {
|
||||||
this.legendContainer.add(
|
this.legendContainer.add(
|
||||||
this.add.text(40, offsetY - 5, NODE_LABELS[type as MapNodeType], { fontSize: '12px', color: '#ffffff' })
|
this.add.text(40, offsetY - 5, NODE_LABELS[type as MapNodeType], { fontSize: '12px', color: '#ffffff' })
|
||||||
);
|
);
|
||||||
offsetY += 22;
|
offsetY += 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hint text
|
// Hint text
|
||||||
this.add.text(width / 2, this.scale.height - 20, '拖拽滚动查看地图', {
|
this.add.text(width / 2, this.scale.height - 20, '拖拽滚动查看地图 (从左到右)', {
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#888888',
|
color: '#888888',
|
||||||
}).setOrigin(0.5).setDepth(100);
|
}).setOrigin(0.5).setDepth(100);
|
||||||
|
|
@ -150,11 +150,11 @@ export class MapViewerScene extends ReactiveScene {
|
||||||
// Update title
|
// Update title
|
||||||
this.titleText.setText(`Map Viewer (Seed: ${this.seed})`);
|
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 maxLayer = 9; // TOTAL_LAYERS - 1 (10 layers: 0-9)
|
||||||
const maxNodesInLayer = 5; // widest layer (settlement has 4 nodes)
|
const maxNodesInLayer = 5; // widest layer (settlement has 4 nodes)
|
||||||
const mapWidth = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
|
const mapWidth = maxLayer * this.LAYER_HEIGHT + 200;
|
||||||
const mapHeight = maxLayer * this.LAYER_HEIGHT + 200;
|
const mapHeight = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
|
||||||
|
|
||||||
// Create scrollable container
|
// Create scrollable container
|
||||||
this.mapContainer = this.add.container(width / 2, height / 2);
|
this.mapContainer = this.add.container(width / 2, height / 2);
|
||||||
|
|
@ -240,14 +240,16 @@ export class MapViewerScene extends ReactiveScene {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNodeX(node: MapNode): number {
|
private getNodeX(node: MapNode): number {
|
||||||
const layer = this.map!.layers[node.layerIndex];
|
// Layers go left-to-right along X axis
|
||||||
const nodeIndex = layer.nodeIds.indexOf(node.id);
|
return -500 + node.layerIndex * this.LAYER_HEIGHT;
|
||||||
const totalNodes = layer.nodeIds.length;
|
|
||||||
const layerWidth = (totalNodes - 1) * this.NODE_SPACING;
|
|
||||||
return -layerWidth / 2 + nodeIndex * this.NODE_SPACING;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNodeY(node: MapNode): number {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { ReactiveScene } from 'boardgame-phaser';
|
import { ReactiveScene } from 'boardgame-phaser';
|
||||||
|
import { MutableSignal } from 'boardgame-core';
|
||||||
import {
|
import {
|
||||||
resolveEncounter,
|
resolveEncounter,
|
||||||
type RunState,
|
type RunState,
|
||||||
|
|
@ -7,18 +8,10 @@ import {
|
||||||
type MapNodeType,
|
type MapNodeType,
|
||||||
} from 'boardgame-core/samples/slay-the-spire-like';
|
} 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 占位符遭遇场景
|
* 占位符遭遇场景
|
||||||
*
|
*
|
||||||
* 当前实现:显示遭遇信息并提供"完成遭遇"按钮
|
* 当前实现:从全局 gameState 读取当前遭遇信息并展示
|
||||||
*
|
*
|
||||||
* 后续扩展:根据 encounter.type 路由到不同的专用遭遇场景
|
* 后续扩展:根据 encounter.type 路由到不同的专用遭遇场景
|
||||||
* - MapNodeType.Minion / Elite → CombatEncounterScene
|
* - MapNodeType.Minion / Elite → CombatEncounterScene
|
||||||
|
|
@ -27,9 +20,13 @@ export interface EncounterData {
|
||||||
* - MapNodeType.Event → EventEncounterScene
|
* - MapNodeType.Event → EventEncounterScene
|
||||||
* - MapNodeType.Curio → CurioEncounterScene
|
* - MapNodeType.Curio → CurioEncounterScene
|
||||||
*/
|
*/
|
||||||
export class PlaceholderEncounterScene extends ReactiveScene<EncounterData> {
|
export class PlaceholderEncounterScene extends ReactiveScene {
|
||||||
constructor() {
|
/** 全局游戏状态(由 App.tsx 注入) */
|
||||||
|
private gameState: MutableSignal<RunState>;
|
||||||
|
|
||||||
|
constructor(gameState: MutableSignal<RunState>) {
|
||||||
super('PlaceholderEncounterScene');
|
super('PlaceholderEncounterScene');
|
||||||
|
this.gameState = gameState;
|
||||||
}
|
}
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
|
|
@ -37,7 +34,24 @@ export class PlaceholderEncounterScene extends ReactiveScene<EncounterData> {
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
const centerX = width / 2;
|
const centerX = width / 2;
|
||||||
const centerY = height / 2;
|
const centerY = height / 2;
|
||||||
const { encounter, nodeId } = this.initData;
|
|
||||||
|
// Read encounter data from global state
|
||||||
|
const state = this.gameState.value;
|
||||||
|
const node = state.map.nodes.get(state.currentNodeId);
|
||||||
|
if (!node || !node.encounter) {
|
||||||
|
this.add.text(centerX, centerY, '没有遭遇数据', {
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#ff4444',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encounter = {
|
||||||
|
type: node.type,
|
||||||
|
name: node.encounter.name,
|
||||||
|
description: node.encounter.description,
|
||||||
|
};
|
||||||
|
const nodeId = node.id;
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
this.add.text(centerX, centerY - 150, '遭遇', {
|
this.add.text(centerX, centerY - 150, '遭遇', {
|
||||||
|
|
@ -94,16 +108,17 @@ export class PlaceholderEncounterScene extends ReactiveScene<EncounterData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async completeEncounter(): Promise<void> {
|
private async completeEncounter(): Promise<void> {
|
||||||
const { runState, nodeId, encounter, onComplete } = this.initData;
|
const state = this.gameState.value;
|
||||||
|
|
||||||
|
// Get current encounter info
|
||||||
|
const node = state.map.nodes.get(state.currentNodeId);
|
||||||
|
if (!node || !node.encounter) return;
|
||||||
|
|
||||||
// 生成模拟遭遇结果
|
// 生成模拟遭遇结果
|
||||||
const result: EncounterResult = this.generatePlaceholderResult(encounter.type);
|
const result: EncounterResult = this.generatePlaceholderResult(node.type);
|
||||||
|
|
||||||
// 调用进度管理器结算遭遇
|
// 调用进度管理器结算遭遇
|
||||||
resolveEncounter(runState, result);
|
resolveEncounter(state, result);
|
||||||
|
|
||||||
// 回调通知上层
|
|
||||||
onComplete(result);
|
|
||||||
|
|
||||||
// 返回游戏流程场景
|
// 返回游戏流程场景
|
||||||
await this.sceneController.launch('GameFlowScene');
|
await this.sceneController.launch('GameFlowScene');
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,14 +7,18 @@ import { GridViewerScene } from '@/scenes/GridViewerScene';
|
||||||
import { ShapeViewerScene } from '@/scenes/ShapeViewerScene';
|
import { ShapeViewerScene } from '@/scenes/ShapeViewerScene';
|
||||||
import { GameFlowScene } from '@/scenes/GameFlowScene';
|
import { GameFlowScene } from '@/scenes/GameFlowScene';
|
||||||
import { PlaceholderEncounterScene } from '@/scenes/PlaceholderEncounterScene';
|
import { PlaceholderEncounterScene } from '@/scenes/PlaceholderEncounterScene';
|
||||||
|
import { createGameState } from '@/state/gameState';
|
||||||
|
|
||||||
|
// 全局游戏状态单例
|
||||||
|
const gameState = createGameState();
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const indexScene = useMemo(() => new IndexScene(), []);
|
const indexScene = useMemo(() => new IndexScene(), []);
|
||||||
const mapViewerScene = useMemo(() => new MapViewerScene(), []);
|
const mapViewerScene = useMemo(() => new MapViewerScene(), []);
|
||||||
const gridViewerScene = useMemo(() => new GridViewerScene(), []);
|
const gridViewerScene = useMemo(() => new GridViewerScene(), []);
|
||||||
const shapeViewerScene = useMemo(() => new ShapeViewerScene(), []);
|
const shapeViewerScene = useMemo(() => new ShapeViewerScene(), []);
|
||||||
const gameFlowScene = useMemo(() => new GameFlowScene(), []);
|
const gameFlowScene = useMemo(() => new GameFlowScene(gameState), []);
|
||||||
const placeholderEncounterScene = useMemo(() => new PlaceholderEncounterScene(), []);
|
const placeholderEncounterScene = useMemo(() => new PlaceholderEncounterScene(gameState), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen">
|
<div className="flex flex-col h-screen">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue