Compare commits
3 Commits
7fb9edcbf0
...
706de4e33a
| Author | SHA1 | Date |
|---|---|---|
|
|
706de4e33a | |
|
|
5342f1c09d | |
|
|
3ca2a16e29 |
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { 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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue