refactor: clean up scenes and introduce SceneKey enum
- Remove redundant `GameFlowScene` and `PlaceholderEncounterScene` - Introduce `SceneKey` enum for type-safe scene management - Clean up unused imports and configuration references in existing scenes - Standardize scene navigation using `SceneKey`
This commit is contained in:
parent
1d803dd219
commit
033a8e4a40
|
|
@ -1,439 +0,0 @@
|
||||||
import Phaser from "phaser";
|
|
||||||
import { ReactiveScene } from "boardgame-phaser";
|
|
||||||
import { createButton } from "@/utils/createButton";
|
|
||||||
import { UI_CONFIG, MAP_CONFIG, NODE_COLORS, NODE_LABELS } from "@/config";
|
|
||||||
import { MutableSignal } from "boardgame-core";
|
|
||||||
import {
|
|
||||||
canMoveTo,
|
|
||||||
moveToNode,
|
|
||||||
getCurrentNode,
|
|
||||||
getReachableChildren,
|
|
||||||
isAtEndNode,
|
|
||||||
type RunState,
|
|
||||||
type MapNode,
|
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
|
||||||
|
|
||||||
export class GameFlowScene extends ReactiveScene {
|
|
||||||
/** 全局游戏状态(由 App.tsx 注入) */
|
|
||||||
private gameState: MutableSignal<RunState>;
|
|
||||||
|
|
||||||
// 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(gameState: MutableSignal<RunState>) {
|
|
||||||
super("GameFlowScene");
|
|
||||||
this.gameState = gameState;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
createButton({
|
|
||||||
scene: this,
|
|
||||||
label: "返回菜单",
|
|
||||||
x: width - 100,
|
|
||||||
y: 25,
|
|
||||||
width: UI_CONFIG.BUTTON_WIDTH_LARGE,
|
|
||||||
height: UI_CONFIG.BUTTON_HEIGHT_LARGE,
|
|
||||||
onClick: async () => {
|
|
||||||
await this.sceneController.launch("IndexScene");
|
|
||||||
},
|
|
||||||
depth: 200,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateHUD(): void {
|
|
||||||
const state = this.gameState.value;
|
|
||||||
const { player, currentNodeId, map } = state;
|
|
||||||
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;
|
|
||||||
const state = this.gameState.value;
|
|
||||||
const {
|
|
||||||
LAYER_HEIGHT,
|
|
||||||
NODE_SPACING,
|
|
||||||
NODE_RADIUS,
|
|
||||||
MAX_NODES_PER_LAYER,
|
|
||||||
TOTAL_LAYERS,
|
|
||||||
} = MAP_CONFIG;
|
|
||||||
|
|
||||||
// Calculate map bounds (left-to-right: layers along X, nodes along Y)
|
|
||||||
const maxLayer = TOTAL_LAYERS - 1;
|
|
||||||
const mapWidth = maxLayer * LAYER_HEIGHT + 200;
|
|
||||||
const mapHeight = (MAX_NODES_PER_LAYER - 1) * NODE_SPACING + 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 } = state;
|
|
||||||
const reachableChildren = getReachableChildren(state);
|
|
||||||
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, NODE_RADIUS);
|
|
||||||
|
|
||||||
if (isCurrent) {
|
|
||||||
nodeGraphics.lineStyle(3, 0xffff44);
|
|
||||||
} else if (isReachable) {
|
|
||||||
nodeGraphics.lineStyle(2, 0xaaddaa);
|
|
||||||
} else {
|
|
||||||
nodeGraphics.lineStyle(2, 0x888888);
|
|
||||||
}
|
|
||||||
nodeGraphics.strokeCircle(posX, posY, 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 + 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, NODE_RADIUS, 0x000000, 0)
|
|
||||||
.setInteractive({ useHandCursor: true });
|
|
||||||
this.mapContainer.add(hitZone);
|
|
||||||
|
|
||||||
hitZone.on("pointerover", () => {
|
|
||||||
this.hoveredNode = nodeId;
|
|
||||||
nodeGraphics.clear();
|
|
||||||
nodeGraphics.fillStyle(this.brightenColor(baseColor));
|
|
||||||
nodeGraphics.fillCircle(posX, posY, NODE_RADIUS);
|
|
||||||
nodeGraphics.lineStyle(3, 0xaaddaa);
|
|
||||||
nodeGraphics.strokeCircle(posX, posY, NODE_RADIUS);
|
|
||||||
});
|
|
||||||
|
|
||||||
hitZone.on("pointerout", () => {
|
|
||||||
this.hoveredNode = null;
|
|
||||||
nodeGraphics.clear();
|
|
||||||
nodeGraphics.fillStyle(baseColor);
|
|
||||||
nodeGraphics.fillCircle(posX, posY, NODE_RADIUS);
|
|
||||||
nodeGraphics.lineStyle(2, 0xaaddaa);
|
|
||||||
nodeGraphics.strokeCircle(posX, posY, NODE_RADIUS);
|
|
||||||
});
|
|
||||||
|
|
||||||
hitZone.on("pointerdown", () => {
|
|
||||||
this.onNodeClick(nodeId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup drag-to-scroll with disposables cleanup
|
|
||||||
const onPointerDown = (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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerMove = (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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerUp = () => {
|
|
||||||
this.isDragging = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.input.on("pointerdown", onPointerDown);
|
|
||||||
this.input.on("pointermove", onPointerMove);
|
|
||||||
this.input.on("pointerup", onPointerUp);
|
|
||||||
this.input.on("pointerout", onPointerUp);
|
|
||||||
|
|
||||||
this.disposables.add(() => {
|
|
||||||
this.input.off("pointerdown", onPointerDown);
|
|
||||||
this.input.off("pointermove", onPointerMove);
|
|
||||||
this.input.off("pointerup", onPointerUp);
|
|
||||||
this.input.off("pointerout", onPointerUp);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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> {
|
|
||||||
const state = this.gameState.value;
|
|
||||||
if (!canMoveTo(state, nodeId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to target node
|
|
||||||
const result = moveToNode(state, nodeId);
|
|
||||||
if (!result.success) {
|
|
||||||
console.warn(`无法移动到节点: ${result.reason}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update visuals
|
|
||||||
this.updateHUD();
|
|
||||||
this.redrawMapHighlights();
|
|
||||||
|
|
||||||
// Check if at end node
|
|
||||||
if (isAtEndNode(state)) {
|
|
||||||
this.showEndScreen();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch encounter scene
|
|
||||||
const currentNode = getCurrentNode(state);
|
|
||||||
if (!currentNode || !currentNode.encounter) {
|
|
||||||
console.warn("当前节点没有遭遇数据");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sceneController.launch("PlaceholderEncounterScene");
|
|
||||||
}
|
|
||||||
|
|
||||||
private redrawMapHighlights(): void {
|
|
||||||
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) {
|
|
||||||
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),
|
|
||||||
MAP_CONFIG.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),
|
|
||||||
MAP_CONFIG.NODE_RADIUS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// End message
|
|
||||||
this.add
|
|
||||||
.text(width / 2, height / 2 - 40, "恭喜通关!", {
|
|
||||||
fontSize: "36px",
|
|
||||||
color: "#ffcc44",
|
|
||||||
fontStyle: "bold",
|
|
||||||
})
|
|
||||||
.setOrigin(0.5)
|
|
||||||
.setDepth(300);
|
|
||||||
|
|
||||||
const { player } = state;
|
|
||||||
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);
|
|
||||||
|
|
||||||
createButton({
|
|
||||||
scene: this,
|
|
||||||
label: "返回菜单",
|
|
||||||
x: width / 2,
|
|
||||||
y: height / 2 + 100,
|
|
||||||
width: UI_CONFIG.BUTTON_WIDTH_LARGE,
|
|
||||||
height: UI_CONFIG.BUTTON_HEIGHT_LARGE,
|
|
||||||
onClick: async () => {
|
|
||||||
await this.sceneController.launch("IndexScene");
|
|
||||||
},
|
|
||||||
depth: 300,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNodeX(node: MapNode): number {
|
|
||||||
return -500 + node.layerIndex * MAP_CONFIG.LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNodeY(node: MapNode): number {
|
|
||||||
const layer = this.gameState.value.map.layers[node.layerIndex];
|
|
||||||
const nodeIndex = layer.nodeIds.indexOf(node.id);
|
|
||||||
const totalNodes = layer.nodeIds.length;
|
|
||||||
const layerHeight = (totalNodes - 1) * MAP_CONFIG.NODE_SPACING;
|
|
||||||
return -layerHeight / 2 + nodeIndex * MAP_CONFIG.NODE_SPACING;
|
|
||||||
}
|
|
||||||
|
|
||||||
private brightenColor(color: number): number {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import Phaser from "phaser";
|
|
||||||
import { ReactiveScene } from "boardgame-phaser";
|
import { ReactiveScene } from "boardgame-phaser";
|
||||||
import { createButton } from "@/utils/createButton";
|
import { createButton } from "@/utils/createButton";
|
||||||
import { GRID_CONFIG, UI_CONFIG, ITEM_COLORS } from "@/config";
|
import { GRID_CONFIG, ITEM_COLORS } from "@/config";
|
||||||
import {
|
import {
|
||||||
createGridInventory,
|
createGridInventory,
|
||||||
placeItem,
|
placeItem,
|
||||||
|
|
@ -12,6 +11,7 @@ import {
|
||||||
type InventoryItem,
|
type InventoryItem,
|
||||||
type GameItemMeta,
|
type GameItemMeta,
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
|
import { SceneKey } from "./types";
|
||||||
|
|
||||||
export class GridViewerScene extends ReactiveScene {
|
export class GridViewerScene extends ReactiveScene {
|
||||||
private inventory: GridInventory<GameItemMeta>;
|
private inventory: GridInventory<GameItemMeta>;
|
||||||
|
|
@ -57,7 +57,7 @@ export class GridViewerScene extends ReactiveScene {
|
||||||
}
|
}
|
||||||
|
|
||||||
private placeSampleItems(): void {
|
private placeSampleItems(): void {
|
||||||
const items = data.desert.items;
|
const items = data.desert.getItems();
|
||||||
const sampleItems = [
|
const sampleItems = [
|
||||||
{ index: 0, x: 0, y: 0 },
|
{ index: 0, x: 0, y: 0 },
|
||||||
{ index: 3, x: 3, y: 0 },
|
{ index: 3, x: 3, y: 0 },
|
||||||
|
|
@ -207,7 +207,7 @@ export class GridViewerScene extends ReactiveScene {
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 40,
|
y: 40,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await this.sceneController.launch("IndexScene");
|
await this.sceneController.launch(SceneKey.IndexScene);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -242,7 +242,7 @@ export class GridViewerScene extends ReactiveScene {
|
||||||
GRID_CONFIG.WIDTH,
|
GRID_CONFIG.WIDTH,
|
||||||
GRID_CONFIG.HEIGHT,
|
GRID_CONFIG.HEIGHT,
|
||||||
);
|
);
|
||||||
const items = data.desert.items;
|
const items = data.desert.getItems();
|
||||||
|
|
||||||
let itemIndex = 0;
|
let itemIndex = 0;
|
||||||
for (let y = 0; y < GRID_CONFIG.HEIGHT && itemIndex < items.length; y++) {
|
for (let y = 0; y < GRID_CONFIG.HEIGHT && itemIndex < items.length; y++) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import Phaser from "phaser";
|
||||||
import { ReactiveScene } from "boardgame-phaser";
|
import { ReactiveScene } from "boardgame-phaser";
|
||||||
import { createButton } from "@/utils/createButton";
|
import { createButton } from "@/utils/createButton";
|
||||||
import { UI_CONFIG } from "@/config";
|
import { UI_CONFIG } from "@/config";
|
||||||
|
import { SceneKey } from "./types";
|
||||||
|
|
||||||
export class IndexScene extends ReactiveScene {
|
export class IndexScene extends ReactiveScene {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -32,15 +33,22 @@ export class IndexScene extends ReactiveScene {
|
||||||
.setOrigin(0.5);
|
.setOrigin(0.5);
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
const buttons = [
|
const buttons: {
|
||||||
{ label: "开始游戏", scene: "GameFlowScene", y: centerY - 70 },
|
label: string;
|
||||||
{ label: "Map Viewer", scene: "MapViewerScene", y: centerY },
|
scene: SceneKey;
|
||||||
|
y: number;
|
||||||
|
}[] = [
|
||||||
|
{ label: "Map Viewer", scene: SceneKey.MapViewerScene, y: centerY },
|
||||||
{
|
{
|
||||||
label: "Grid Inventory Viewer",
|
label: "Grid Inventory Viewer",
|
||||||
scene: "GridViewerScene",
|
scene: SceneKey.GridViewerScene,
|
||||||
y: centerY + 70,
|
y: centerY + 70,
|
||||||
},
|
},
|
||||||
{ label: "Shape Viewer", scene: "ShapeViewerScene", y: centerY + 140 },
|
{
|
||||||
|
label: "Shape Viewer",
|
||||||
|
scene: SceneKey.ShapeViewerScene,
|
||||||
|
y: centerY + 140,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const btn of buttons) {
|
for (const btn of buttons) {
|
||||||
|
|
@ -50,7 +58,7 @@ export class IndexScene extends ReactiveScene {
|
||||||
|
|
||||||
private createButton(
|
private createButton(
|
||||||
label: string,
|
label: string,
|
||||||
targetScene: string,
|
targetScene: SceneKey,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): void {
|
): void {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
type MapNode,
|
type MapNode,
|
||||||
type MapNodeType,
|
type MapNodeType,
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
|
import { SceneKey } from "./types";
|
||||||
|
|
||||||
export class MapViewerScene extends ReactiveScene {
|
export class MapViewerScene extends ReactiveScene {
|
||||||
private map: PointCrawlMap | null = null;
|
private map: PointCrawlMap | null = null;
|
||||||
|
|
@ -57,7 +58,7 @@ export class MapViewerScene extends ReactiveScene {
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 40,
|
y: 40,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await this.sceneController.launch("IndexScene");
|
await this.sceneController.launch(SceneKey.IndexScene);
|
||||||
},
|
},
|
||||||
depth: 100,
|
depth: 100,
|
||||||
});
|
});
|
||||||
|
|
@ -113,7 +114,7 @@ export class MapViewerScene extends ReactiveScene {
|
||||||
|
|
||||||
private drawMap(): void {
|
private drawMap(): void {
|
||||||
const rng = createRNG(this.seed);
|
const rng = createRNG(this.seed);
|
||||||
this.map = generatePointCrawlMap(rng, data.desert.encounters);
|
this.map = generatePointCrawlMap(rng, data.desert.getEncounters());
|
||||||
|
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
const {
|
const {
|
||||||
|
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
import {
|
|
||||||
resolveEncounter,
|
|
||||||
type RunState,
|
|
||||||
type EncounterResult,
|
|
||||||
type MapNodeType,
|
|
||||||
type MapNode,
|
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
|
||||||
import { ReactiveScene } from "boardgame-phaser";
|
|
||||||
|
|
||||||
import type { MutableSignal } from "boardgame-core";
|
|
||||||
|
|
||||||
import { UI_CONFIG, GRID_CONFIG, NODE_COLORS, NODE_LABELS } from "@/config";
|
|
||||||
import { createButton } from "@/utils/createButton";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 占位符遭遇场景
|
|
||||||
*/
|
|
||||||
export class PlaceholderEncounterScene extends ReactiveScene {
|
|
||||||
private gameState: MutableSignal<RunState>;
|
|
||||||
|
|
||||||
constructor(gameState: MutableSignal<RunState>) {
|
|
||||||
super("PlaceholderEncounterScene");
|
|
||||||
this.gameState = gameState;
|
|
||||||
}
|
|
||||||
|
|
||||||
create(): void {
|
|
||||||
super.create();
|
|
||||||
const { width, height } = this.scale;
|
|
||||||
const state = this.gameState.value;
|
|
||||||
|
|
||||||
const gridCols = state.inventory.width;
|
|
||||||
const gridRows = state.inventory.height;
|
|
||||||
const cellSize = GRID_CONFIG.WIDGET_CELL_SIZE;
|
|
||||||
const gridW = gridCols * cellSize + (gridCols - 1) * GRID_CONFIG.GRID_GAP;
|
|
||||||
const gridH = gridRows * cellSize + (gridRows - 1) * GRID_CONFIG.GRID_GAP;
|
|
||||||
const leftPanelW = gridW + 40;
|
|
||||||
|
|
||||||
this.cameras.main.setBounds(0, 0, width, height);
|
|
||||||
this.cameras.main.setScroll(0, 0);
|
|
||||||
|
|
||||||
// "背包" title
|
|
||||||
this.add
|
|
||||||
.text(60 + gridW / 2, (height - gridH) / 2, "背包", {
|
|
||||||
fontSize: "22px",
|
|
||||||
color: "#ffffff",
|
|
||||||
fontStyle: "bold",
|
|
||||||
})
|
|
||||||
.setOrigin(0.5);
|
|
||||||
|
|
||||||
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 as MapNode & { encounter: { name: string; description: string } },
|
|
||||||
leftPanelW,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawRightPanel(
|
|
||||||
node: MapNode & { encounter: { name: string; description: string } },
|
|
||||||
leftPanelW: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
): void {
|
|
||||||
const encounter = {
|
|
||||||
type: node.type as MapNodeType,
|
|
||||||
name: node.encounter.name,
|
|
||||||
description: node.encounter.description,
|
|
||||||
};
|
|
||||||
const nodeId = node.id as string;
|
|
||||||
|
|
||||||
const rightX = leftPanelW + 60;
|
|
||||||
const rightW = width - rightX - 40;
|
|
||||||
const cx = rightX + rightW / 2;
|
|
||||||
const cy = height / 2;
|
|
||||||
|
|
||||||
this.add
|
|
||||||
.text(cx, cy - 180, "遭遇", {
|
|
||||||
fontSize: "36px",
|
|
||||||
color: "#fff",
|
|
||||||
fontStyle: "bold",
|
|
||||||
})
|
|
||||||
.setOrigin(0.5);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
this.add
|
|
||||||
.text(cx, cy - 50, encounter.name, {
|
|
||||||
fontSize: "28px",
|
|
||||||
color: "#fff",
|
|
||||||
})
|
|
||||||
.setOrigin(0.5);
|
|
||||||
|
|
||||||
this.add
|
|
||||||
.text(cx, cy + 10, encounter.description || "(暂无描述)", {
|
|
||||||
fontSize: "18px",
|
|
||||||
color: "#bbb",
|
|
||||||
wordWrap: { width: rightW - 40 },
|
|
||||||
align: "center",
|
|
||||||
})
|
|
||||||
.setOrigin(0.5);
|
|
||||||
|
|
||||||
this.add
|
|
||||||
.text(cx, cy + 80, `节点: ${nodeId}`, {
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "#666",
|
|
||||||
})
|
|
||||||
.setOrigin(0.5);
|
|
||||||
|
|
||||||
this.add
|
|
||||||
.text(cx, cy + 130, "(此为占位符遭遇,后续将替换为真实遭遇场景)", {
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "#ff8844",
|
|
||||||
fontStyle: "italic",
|
|
||||||
})
|
|
||||||
.setOrigin(0.5);
|
|
||||||
|
|
||||||
createButton({
|
|
||||||
scene: this,
|
|
||||||
label: "完成遭遇",
|
|
||||||
x: cx,
|
|
||||||
y: cy + 200,
|
|
||||||
width: UI_CONFIG.BUTTON_WIDTH_LARGE,
|
|
||||||
height: UI_CONFIG.BUTTON_HEIGHT_LARGE,
|
|
||||||
onClick: async () => {
|
|
||||||
await this.completeEncounter();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createButton({
|
|
||||||
scene: this,
|
|
||||||
label: "暂不处理",
|
|
||||||
x: cx,
|
|
||||||
y: cy + 270,
|
|
||||||
onClick: async () => {
|
|
||||||
await this.sceneController.launch("GameFlowScene");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTypeLabel(type: MapNodeType): string {
|
|
||||||
return NODE_LABELS[type] ?? type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTypeColor(type: MapNodeType): number {
|
|
||||||
return NODE_COLORS[type] ?? 0x888888;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async completeEncounter(): Promise<void> {
|
|
||||||
const state = this.gameState.value;
|
|
||||||
const node = state.map.nodes.get(state.currentNodeId);
|
|
||||||
if (!node || !node.encounter) return;
|
|
||||||
|
|
||||||
const result: EncounterResult = this.generatePlaceholderResult(node.type);
|
|
||||||
resolveEncounter(state, result);
|
|
||||||
await this.sceneController.launch("GameFlowScene");
|
|
||||||
}
|
|
||||||
|
|
||||||
private generatePlaceholderResult(type: MapNodeType): EncounterResult {
|
|
||||||
switch (type) {
|
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import Phaser from "phaser";
|
|
||||||
import { ReactiveScene } from "boardgame-phaser";
|
import { ReactiveScene } from "boardgame-phaser";
|
||||||
import { createButton } from "@/utils/createButton";
|
import { createButton } from "@/utils/createButton";
|
||||||
import { UI_CONFIG, SHAPE_CONFIG, NODE_LABELS, NODE_COLORS } from "@/config";
|
import { SHAPE_CONFIG } from "@/config";
|
||||||
import {
|
import {
|
||||||
parseShapeString,
|
parseShapeString,
|
||||||
data,
|
data,
|
||||||
type ParsedShape,
|
type ParsedShape,
|
||||||
} from "boardgame-core/samples/slay-the-spire-like";
|
} from "boardgame-core/samples/slay-the-spire-like";
|
||||||
|
import { SceneKey } from "./types";
|
||||||
|
|
||||||
export class ShapeViewerScene extends ReactiveScene {
|
export class ShapeViewerScene extends ReactiveScene {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -40,7 +40,7 @@ export class ShapeViewerScene extends ReactiveScene {
|
||||||
const startY = 80;
|
const startY = 80;
|
||||||
const { SPACING_X, SPACING_Y, ITEMS_PER_ROW, MAX_ITEMS } = SHAPE_CONFIG;
|
const { SPACING_X, SPACING_Y, ITEMS_PER_ROW, MAX_ITEMS } = SHAPE_CONFIG;
|
||||||
|
|
||||||
const itemsToShow = data.desert.items.slice(0, MAX_ITEMS);
|
const itemsToShow = data.desert.getItems().slice(0, MAX_ITEMS);
|
||||||
|
|
||||||
for (let i = 0; i < itemsToShow.length; i++) {
|
for (let i = 0; i < itemsToShow.length; i++) {
|
||||||
const itemData = itemsToShow[i];
|
const itemData = itemsToShow[i];
|
||||||
|
|
@ -145,7 +145,7 @@ export class ShapeViewerScene extends ReactiveScene {
|
||||||
x: 100,
|
x: 100,
|
||||||
y: height - 40,
|
y: height - 40,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await this.sceneController.launch("IndexScene");
|
await this.sceneController.launch(SceneKey.IndexScene);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export enum SceneKey {
|
||||||
|
GridViewerScene = "GridViewerScene",
|
||||||
|
IndexScene = "IndexScene",
|
||||||
|
MapViewerScene = "MapViewerScene",
|
||||||
|
ShapeViewerScene = "ShapeViewerScene",
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue