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 { createButton } from "@/utils/createButton";
|
||||
import { GRID_CONFIG, UI_CONFIG, ITEM_COLORS } from "@/config";
|
||||
import { GRID_CONFIG, ITEM_COLORS } from "@/config";
|
||||
import {
|
||||
createGridInventory,
|
||||
placeItem,
|
||||
|
|
@ -12,6 +11,7 @@ import {
|
|||
type InventoryItem,
|
||||
type GameItemMeta,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { SceneKey } from "./types";
|
||||
|
||||
export class GridViewerScene extends ReactiveScene {
|
||||
private inventory: GridInventory<GameItemMeta>;
|
||||
|
|
@ -57,7 +57,7 @@ export class GridViewerScene extends ReactiveScene {
|
|||
}
|
||||
|
||||
private placeSampleItems(): void {
|
||||
const items = data.desert.items;
|
||||
const items = data.desert.getItems();
|
||||
const sampleItems = [
|
||||
{ index: 0, x: 0, y: 0 },
|
||||
{ index: 3, x: 3, y: 0 },
|
||||
|
|
@ -207,7 +207,7 @@ export class GridViewerScene extends ReactiveScene {
|
|||
x: 100,
|
||||
y: 40,
|
||||
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.HEIGHT,
|
||||
);
|
||||
const items = data.desert.items;
|
||||
const items = data.desert.getItems();
|
||||
|
||||
let itemIndex = 0;
|
||||
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 { createButton } from "@/utils/createButton";
|
||||
import { UI_CONFIG } from "@/config";
|
||||
import { SceneKey } from "./types";
|
||||
|
||||
export class IndexScene extends ReactiveScene {
|
||||
constructor() {
|
||||
|
|
@ -32,15 +33,22 @@ export class IndexScene extends ReactiveScene {
|
|||
.setOrigin(0.5);
|
||||
|
||||
// Buttons
|
||||
const buttons = [
|
||||
{ label: "开始游戏", scene: "GameFlowScene", y: centerY - 70 },
|
||||
{ label: "Map Viewer", scene: "MapViewerScene", y: centerY },
|
||||
const buttons: {
|
||||
label: string;
|
||||
scene: SceneKey;
|
||||
y: number;
|
||||
}[] = [
|
||||
{ label: "Map Viewer", scene: SceneKey.MapViewerScene, y: centerY },
|
||||
{
|
||||
label: "Grid Inventory Viewer",
|
||||
scene: "GridViewerScene",
|
||||
scene: SceneKey.GridViewerScene,
|
||||
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) {
|
||||
|
|
@ -50,7 +58,7 @@ export class IndexScene extends ReactiveScene {
|
|||
|
||||
private createButton(
|
||||
label: string,
|
||||
targetScene: string,
|
||||
targetScene: SceneKey,
|
||||
x: number,
|
||||
y: number,
|
||||
): void {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
type MapNode,
|
||||
type MapNodeType,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { SceneKey } from "./types";
|
||||
|
||||
export class MapViewerScene extends ReactiveScene {
|
||||
private map: PointCrawlMap | null = null;
|
||||
|
|
@ -57,7 +58,7 @@ export class MapViewerScene extends ReactiveScene {
|
|||
x: 100,
|
||||
y: 40,
|
||||
onClick: async () => {
|
||||
await this.sceneController.launch("IndexScene");
|
||||
await this.sceneController.launch(SceneKey.IndexScene);
|
||||
},
|
||||
depth: 100,
|
||||
});
|
||||
|
|
@ -113,7 +114,7 @@ export class MapViewerScene extends ReactiveScene {
|
|||
|
||||
private drawMap(): void {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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 { createButton } from "@/utils/createButton";
|
||||
import { UI_CONFIG, SHAPE_CONFIG, NODE_LABELS, NODE_COLORS } from "@/config";
|
||||
import { SHAPE_CONFIG } from "@/config";
|
||||
import {
|
||||
parseShapeString,
|
||||
data,
|
||||
type ParsedShape,
|
||||
} from "boardgame-core/samples/slay-the-spire-like";
|
||||
import { SceneKey } from "./types";
|
||||
|
||||
export class ShapeViewerScene extends ReactiveScene {
|
||||
constructor() {
|
||||
|
|
@ -40,7 +40,7 @@ export class ShapeViewerScene extends ReactiveScene {
|
|||
const startY = 80;
|
||||
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++) {
|
||||
const itemData = itemsToShow[i];
|
||||
|
|
@ -145,7 +145,7 @@ export class ShapeViewerScene extends ReactiveScene {
|
|||
x: 100,
|
||||
y: height - 40,
|
||||
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