feat: add game process scene
This commit is contained in:
parent
65684e6cf5
commit
7fb9edcbf0
|
|
@ -0,0 +1,437 @@
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import { ReactiveScene } from 'boardgame-phaser';
|
||||||
|
import {
|
||||||
|
createRunState,
|
||||||
|
canMoveTo,
|
||||||
|
moveToNode,
|
||||||
|
getCurrentNode,
|
||||||
|
getReachableChildren,
|
||||||
|
isAtEndNode,
|
||||||
|
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,
|
||||||
|
end: 0xcc8844,
|
||||||
|
minion: 0xcc4444,
|
||||||
|
elite: 0xcc44cc,
|
||||||
|
event: 0xaaaa44,
|
||||||
|
camp: 0x44cccc,
|
||||||
|
shop: 0x4488cc,
|
||||||
|
curio: 0x8844cc,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NODE_LABELS: Record<string, string> = {
|
||||||
|
start: '起点',
|
||||||
|
end: '终点',
|
||||||
|
minion: '战斗',
|
||||||
|
elite: '精英',
|
||||||
|
event: '事件',
|
||||||
|
camp: '营地',
|
||||||
|
shop: '商店',
|
||||||
|
curio: '奇遇',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GameFlowScene extends ReactiveScene {
|
||||||
|
private runState: RunState;
|
||||||
|
private seed: number;
|
||||||
|
|
||||||
|
// Layout constants
|
||||||
|
private readonly LAYER_HEIGHT = 110;
|
||||||
|
private readonly NODE_SPACING = 140;
|
||||||
|
private readonly NODE_RADIUS = 28;
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
private hudContainer!: Phaser.GameObjects.Container;
|
||||||
|
private hpText!: Phaser.GameObjects.Text;
|
||||||
|
private goldText!: Phaser.GameObjects.Text;
|
||||||
|
private nodeText!: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
// Map elements
|
||||||
|
private mapContainer!: Phaser.GameObjects.Container;
|
||||||
|
private isDragging = false;
|
||||||
|
private dragStartX = 0;
|
||||||
|
private dragStartY = 0;
|
||||||
|
private dragStartContainerX = 0;
|
||||||
|
private dragStartContainerY = 0;
|
||||||
|
|
||||||
|
// Interaction
|
||||||
|
private hoveredNode: string | null = null;
|
||||||
|
private nodeGraphics: Map<string, Phaser.GameObjects.Graphics> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('GameFlowScene');
|
||||||
|
this.seed = Date.now();
|
||||||
|
this.runState = createRunState(this.seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
super.create();
|
||||||
|
this.drawHUD();
|
||||||
|
this.drawMap();
|
||||||
|
this.updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawHUD(): void {
|
||||||
|
const { width } = this.scale;
|
||||||
|
|
||||||
|
// HUD background
|
||||||
|
const hudBg = this.add.rectangle(width / 2, 25, 400, 40, 0x111122, 0.8);
|
||||||
|
this.hudContainer = this.add.container(width / 2, 25).setDepth(200);
|
||||||
|
this.hudContainer.add(hudBg);
|
||||||
|
|
||||||
|
// HP
|
||||||
|
this.hpText = this.add.text(-150, 0, '', {
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#ff6666',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
this.hudContainer.add(this.hpText);
|
||||||
|
|
||||||
|
// Gold
|
||||||
|
this.goldText = this.add.text(-50, 0, '', {
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#ffcc44',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
this.hudContainer.add(this.goldText);
|
||||||
|
|
||||||
|
// Current node
|
||||||
|
this.nodeText = this.add.text(50, 0, '', {
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#ffffff',
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
this.hudContainer.add(this.nodeText);
|
||||||
|
|
||||||
|
// Back to menu button
|
||||||
|
this.createButton('返回菜单', width - 100, 25, 140, 36, async () => {
|
||||||
|
await this.sceneController.launch('IndexScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateHUD(): void {
|
||||||
|
const { player, currentNodeId, map } = this.runState;
|
||||||
|
const currentNode = map.nodes.get(currentNodeId);
|
||||||
|
|
||||||
|
this.hpText.setText(`HP: ${player.currentHp}/${player.maxHp}`);
|
||||||
|
this.goldText.setText(`💰 ${player.gold}`);
|
||||||
|
|
||||||
|
if (currentNode) {
|
||||||
|
const typeLabel = NODE_LABELS[currentNode.type] ?? currentNode.type;
|
||||||
|
const encounterName = currentNode.encounter?.name ?? typeLabel;
|
||||||
|
this.nodeText.setText(`当前: ${encounterName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawMap(): void {
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
|
||||||
|
// Calculate map bounds
|
||||||
|
const maxLayer = 9;
|
||||||
|
const maxNodesInLayer = 5;
|
||||||
|
const mapWidth = (maxNodesInLayer - 1) * this.NODE_SPACING + 200;
|
||||||
|
const mapHeight = maxLayer * this.LAYER_HEIGHT + 200;
|
||||||
|
|
||||||
|
// Create scrollable container
|
||||||
|
this.mapContainer = this.add.container(width / 2, height / 2 + 50);
|
||||||
|
|
||||||
|
// Background panel
|
||||||
|
const bg = this.add.rectangle(0, 0, mapWidth, mapHeight, 0x111122, 0.5).setOrigin(0.5);
|
||||||
|
this.mapContainer.add(bg);
|
||||||
|
|
||||||
|
const graphics = this.add.graphics();
|
||||||
|
this.mapContainer.add(graphics);
|
||||||
|
|
||||||
|
const { map, currentNodeId } = this.runState;
|
||||||
|
const reachableChildren = getReachableChildren(this.runState);
|
||||||
|
const reachableIds = new Set(reachableChildren.map(n => n.id));
|
||||||
|
|
||||||
|
// Draw edges
|
||||||
|
graphics.lineStyle(2, 0x666666);
|
||||||
|
for (const [nodeId, node] of map.nodes) {
|
||||||
|
const posX = this.getNodeX(node);
|
||||||
|
const posY = this.getNodeY(node);
|
||||||
|
|
||||||
|
for (const childId of node.childIds) {
|
||||||
|
const child = map.nodes.get(childId);
|
||||||
|
if (child) {
|
||||||
|
const childX = this.getNodeX(child);
|
||||||
|
const childY = this.getNodeY(child);
|
||||||
|
graphics.lineBetween(posX, posY, childX, childY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
for (const [nodeId, node] of map.nodes) {
|
||||||
|
const posX = this.getNodeX(node);
|
||||||
|
const posY = this.getNodeY(node);
|
||||||
|
const isCurrent = nodeId === currentNodeId;
|
||||||
|
const isReachable = reachableIds.has(nodeId);
|
||||||
|
const baseColor = NODE_COLORS[node.type] ?? 0x888888;
|
||||||
|
|
||||||
|
// Node circle
|
||||||
|
const nodeGraphics = this.add.graphics();
|
||||||
|
this.mapContainer.add(nodeGraphics);
|
||||||
|
this.nodeGraphics.set(nodeId, nodeGraphics);
|
||||||
|
|
||||||
|
const color = isCurrent ? 0xffffff : (isReachable ? this.brightenColor(baseColor) : baseColor);
|
||||||
|
nodeGraphics.fillStyle(color);
|
||||||
|
nodeGraphics.fillCircle(posX, posY, this.NODE_RADIUS);
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
nodeGraphics.lineStyle(3, 0xffff44);
|
||||||
|
} else if (isReachable) {
|
||||||
|
nodeGraphics.lineStyle(2, 0xaaddaa);
|
||||||
|
} else {
|
||||||
|
nodeGraphics.lineStyle(2, 0x888888);
|
||||||
|
}
|
||||||
|
nodeGraphics.strokeCircle(posX, posY, this.NODE_RADIUS);
|
||||||
|
|
||||||
|
// Node label
|
||||||
|
const label = NODE_LABELS[node.type] ?? node.type;
|
||||||
|
this.mapContainer.add(
|
||||||
|
this.add.text(posX, posY, label, {
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontStyle: isCurrent ? 'bold' : 'normal',
|
||||||
|
}).setOrigin(0.5)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Encounter name
|
||||||
|
if (node.encounter) {
|
||||||
|
this.mapContainer.add(
|
||||||
|
this.add.text(posX, posY + this.NODE_RADIUS + 12, node.encounter.name, {
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#cccccc',
|
||||||
|
}).setOrigin(0.5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make reachable nodes interactive
|
||||||
|
if (isReachable) {
|
||||||
|
const hitZone = this.add.circle(posX, posY, this.NODE_RADIUS, 0x000000, 0)
|
||||||
|
.setInteractive({ useHandCursor: true });
|
||||||
|
|
||||||
|
hitZone.on('pointerover', () => {
|
||||||
|
this.hoveredNode = nodeId;
|
||||||
|
nodeGraphics.clear();
|
||||||
|
nodeGraphics.fillStyle(this.brightenColor(baseColor));
|
||||||
|
nodeGraphics.fillCircle(posX, posY, this.NODE_RADIUS);
|
||||||
|
nodeGraphics.lineStyle(3, 0xaaddaa);
|
||||||
|
nodeGraphics.strokeCircle(posX, posY, this.NODE_RADIUS);
|
||||||
|
});
|
||||||
|
|
||||||
|
hitZone.on('pointerout', () => {
|
||||||
|
this.hoveredNode = null;
|
||||||
|
nodeGraphics.clear();
|
||||||
|
nodeGraphics.fillStyle(baseColor);
|
||||||
|
nodeGraphics.fillCircle(posX, posY, this.NODE_RADIUS);
|
||||||
|
nodeGraphics.lineStyle(2, 0xaaddaa);
|
||||||
|
nodeGraphics.strokeCircle(posX, posY, this.NODE_RADIUS);
|
||||||
|
});
|
||||||
|
|
||||||
|
hitZone.on('pointerdown', () => {
|
||||||
|
this.onNodeClick(nodeId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup drag-to-scroll
|
||||||
|
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||||
|
this.isDragging = true;
|
||||||
|
this.dragStartX = pointer.x;
|
||||||
|
this.dragStartY = pointer.y;
|
||||||
|
this.dragStartContainerX = this.mapContainer.x;
|
||||||
|
this.dragStartContainerY = this.mapContainer.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => {
|
||||||
|
if (!this.isDragging) return;
|
||||||
|
this.mapContainer.x = this.dragStartContainerX + (pointer.x - this.dragStartX);
|
||||||
|
this.mapContainer.y = this.dragStartContainerY + (pointer.y - this.dragStartY);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.input.on('pointerup', () => {
|
||||||
|
this.isDragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.input.on('pointerout', () => {
|
||||||
|
this.isDragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hint text
|
||||||
|
this.add.text(width / 2, this.scale.height - 20, '点击可到达的节点进入遭遇 | 拖拽滚动查看地图', {
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#888888',
|
||||||
|
}).setOrigin(0.5).setDepth(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onNodeClick(nodeId: string): Promise<void> {
|
||||||
|
if (!canMoveTo(this.runState, nodeId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to target node
|
||||||
|
const result = moveToNode(this.runState, nodeId);
|
||||||
|
if (!result.success) {
|
||||||
|
console.warn(`无法移动到节点: ${result.reason}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update visuals
|
||||||
|
this.updateHUD();
|
||||||
|
this.redrawMapHighlights();
|
||||||
|
|
||||||
|
// Check if at end node
|
||||||
|
if (isAtEndNode(this.runState)) {
|
||||||
|
this.showEndScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch encounter scene
|
||||||
|
const currentNode = getCurrentNode(this.runState);
|
||||||
|
if (!currentNode || !currentNode.encounter) {
|
||||||
|
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 reachableIds = new Set(reachableChildren.map(n => n.id));
|
||||||
|
|
||||||
|
for (const [nodeId, nodeGraphics] of this.nodeGraphics) {
|
||||||
|
const node = map.nodes.get(nodeId);
|
||||||
|
if (!node) continue;
|
||||||
|
|
||||||
|
const isCurrent = nodeId === currentNodeId;
|
||||||
|
const isReachable = reachableIds.has(nodeId);
|
||||||
|
const baseColor = NODE_COLORS[node.type] ?? 0x888888;
|
||||||
|
|
||||||
|
nodeGraphics.clear();
|
||||||
|
const color = isCurrent ? 0xffffff : (isReachable ? this.brightenColor(baseColor) : baseColor);
|
||||||
|
nodeGraphics.fillStyle(color);
|
||||||
|
nodeGraphics.fillCircle(this.getNodeX(node), this.getNodeY(node), this.NODE_RADIUS);
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
nodeGraphics.lineStyle(3, 0xffff44);
|
||||||
|
} else if (isReachable) {
|
||||||
|
nodeGraphics.lineStyle(2, 0xaaddaa);
|
||||||
|
} else {
|
||||||
|
nodeGraphics.lineStyle(2, 0x888888);
|
||||||
|
}
|
||||||
|
nodeGraphics.strokeCircle(this.getNodeX(node), this.getNodeY(node), this.NODE_RADIUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showEndScreen(): void {
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
|
||||||
|
// Overlay
|
||||||
|
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(300);
|
||||||
|
|
||||||
|
// End message
|
||||||
|
this.add.text(width / 2, height / 2 - 40, '恭喜通关!', {
|
||||||
|
fontSize: '36px',
|
||||||
|
color: '#ffcc44',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}).setOrigin(0.5).setDepth(300);
|
||||||
|
|
||||||
|
const { player } = this.runState;
|
||||||
|
this.add.text(width / 2, height / 2 + 20, `剩余 HP: ${player.currentHp}/${player.maxHp}\n剩余金币: ${player.gold}`, {
|
||||||
|
fontSize: '20px',
|
||||||
|
color: '#ffffff',
|
||||||
|
align: 'center',
|
||||||
|
}).setOrigin(0.5).setDepth(300);
|
||||||
|
|
||||||
|
this.createButton('返回菜单', width / 2, height / 2 + 100, 200, 50, async () => {
|
||||||
|
await this.sceneController.launch('IndexScene');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNodeY(node: MapNode): number {
|
||||||
|
return -600 + node.layerIndex * this.LAYER_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private brightenColor(color: number): number {
|
||||||
|
// Simple color brightening
|
||||||
|
const r = Math.min(255, ((color >> 16) & 0xff) + 40);
|
||||||
|
const g = Math.min(255, ((color >> 8) & 0xff) + 40);
|
||||||
|
const b = Math.min(255, (color & 0xff) + 40);
|
||||||
|
return (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createButton(
|
||||||
|
label: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
onClick: () => void,
|
||||||
|
depth: number = 200
|
||||||
|
): void {
|
||||||
|
const bg = this.add.rectangle(x, y, width, height, 0x444466)
|
||||||
|
.setStrokeStyle(2, 0x7777aa)
|
||||||
|
.setInteractive({ useHandCursor: true })
|
||||||
|
.setDepth(depth);
|
||||||
|
|
||||||
|
const text = this.add.text(x, y, label, {
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#ffffff',
|
||||||
|
}).setOrigin(0.5).setDepth(depth);
|
||||||
|
|
||||||
|
bg.on('pointerover', () => {
|
||||||
|
bg.setFillStyle(0x555588);
|
||||||
|
text.setScale(1.05);
|
||||||
|
});
|
||||||
|
|
||||||
|
bg.on('pointerout', () => {
|
||||||
|
bg.setFillStyle(0x444466);
|
||||||
|
text.setScale(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
bg.on('pointerdown', onClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,9 +27,10 @@ export class IndexScene extends ReactiveScene {
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{ label: 'Map Viewer', scene: 'MapViewerScene', y: centerY - 20 },
|
{ label: '开始游戏', scene: 'GameFlowScene', y: centerY - 70 },
|
||||||
{ label: 'Grid Inventory Viewer', scene: 'GridViewerScene', y: centerY + 50 },
|
{ label: 'Map Viewer', scene: 'MapViewerScene', y: centerY },
|
||||||
{ label: 'Shape Viewer', scene: 'ShapeViewerScene', y: centerY + 120 },
|
{ label: 'Grid Inventory Viewer', scene: 'GridViewerScene', y: centerY + 70 },
|
||||||
|
{ label: 'Shape Viewer', scene: 'ShapeViewerScene', y: centerY + 140 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const btn of buttons) {
|
for (const btn of buttons) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import { ReactiveScene } from 'boardgame-phaser';
|
||||||
|
import {
|
||||||
|
resolveEncounter,
|
||||||
|
type RunState,
|
||||||
|
type EncounterResult,
|
||||||
|
type MapNodeType,
|
||||||
|
} 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
|
||||||
|
*/
|
||||||
|
export class PlaceholderEncounterScene extends ReactiveScene<EncounterData> {
|
||||||
|
constructor() {
|
||||||
|
super('PlaceholderEncounterScene');
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
super.create();
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
const centerX = width / 2;
|
||||||
|
const centerY = height / 2;
|
||||||
|
const { encounter, nodeId } = this.initData;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
this.add.text(centerX, centerY - 150, '遭遇', {
|
||||||
|
fontSize: '32px',
|
||||||
|
color: '#ffffff',
|
||||||
|
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',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Encounter name
|
||||||
|
this.add.text(centerX, centerY - 30, encounter.name, {
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#ffffff',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Encounter description
|
||||||
|
this.add.text(centerX, centerY + 20, encounter.description || '(暂无描述)', {
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#aaaaaa',
|
||||||
|
wordWrap: { width: 600 },
|
||||||
|
align: 'center',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Node ID info
|
||||||
|
this.add.text(centerX, centerY + 80, `节点: ${nodeId}`, {
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666666',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Placeholder notice
|
||||||
|
this.add.text(centerX, centerY + 130, '(此为占位符遭遇,后续将替换为真实遭遇场景)', {
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#ff8844',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
// Complete button
|
||||||
|
this.createButton('完成遭遇', centerX, centerY + 200, 200, 50, async () => {
|
||||||
|
await this.completeEncounter();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Back button (without resolving)
|
||||||
|
this.createButton('暂不处理', centerX, centerY + 270, 200, 40, async () => {
|
||||||
|
await this.sceneController.launch('GameFlowScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async completeEncounter(): Promise<void> {
|
||||||
|
const { runState, nodeId, encounter, onComplete } = this.initData;
|
||||||
|
|
||||||
|
// 生成模拟遭遇结果
|
||||||
|
const result: EncounterResult = this.generatePlaceholderResult(encounter.type);
|
||||||
|
|
||||||
|
// 调用进度管理器结算遭遇
|
||||||
|
resolveEncounter(runState, result);
|
||||||
|
|
||||||
|
// 回调通知上层
|
||||||
|
onComplete(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 'curio':
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,16 @@ import { IndexScene } from '@/scenes/IndexScene';
|
||||||
import { MapViewerScene } from '@/scenes/MapViewerScene';
|
import { MapViewerScene } from '@/scenes/MapViewerScene';
|
||||||
import { GridViewerScene } from '@/scenes/GridViewerScene';
|
import { GridViewerScene } from '@/scenes/GridViewerScene';
|
||||||
import { ShapeViewerScene } from '@/scenes/ShapeViewerScene';
|
import { ShapeViewerScene } from '@/scenes/ShapeViewerScene';
|
||||||
|
import { GameFlowScene } from '@/scenes/GameFlowScene';
|
||||||
|
import { PlaceholderEncounterScene } from '@/scenes/PlaceholderEncounterScene';
|
||||||
|
|
||||||
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 placeholderEncounterScene = useMemo(() => new PlaceholderEncounterScene(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen">
|
<div className="flex flex-col h-screen">
|
||||||
|
|
@ -20,6 +24,8 @@ export default function App() {
|
||||||
<PhaserScene sceneKey="MapViewerScene" scene={mapViewerScene} />
|
<PhaserScene sceneKey="MapViewerScene" scene={mapViewerScene} />
|
||||||
<PhaserScene sceneKey="GridViewerScene" scene={gridViewerScene} />
|
<PhaserScene sceneKey="GridViewerScene" scene={gridViewerScene} />
|
||||||
<PhaserScene sceneKey="ShapeViewerScene" scene={shapeViewerScene} />
|
<PhaserScene sceneKey="ShapeViewerScene" scene={shapeViewerScene} />
|
||||||
|
<PhaserScene sceneKey="GameFlowScene" scene={gameFlowScene} />
|
||||||
|
<PhaserScene sceneKey="PlaceholderEncounterScene" scene={placeholderEncounterScene} />
|
||||||
</PhaserGame>
|
</PhaserGame>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue