diff --git a/src/stages/page_chase.ts b/src/stages/page_chase.ts new file mode 100644 index 0000000..b2d8ec1 --- /dev/null +++ b/src/stages/page_chase.ts @@ -0,0 +1,84 @@ +import { BaseScene } from "@/scene/BaseScene"; +import { SceneType } from "@/enums/SceneType"; +import { ChaseGameModel } from "@/game/chase/model"; +import type { NodeId } from "@/game/chase/types"; +import { Container, Graphics, Text, TextStyle } from "pixi.js"; + +const NODE_RADIUS = 18; +const NODE_GAP = 58; + +export default class ChaseScene extends BaseScene { + stage = new Container(); + private model?: ChaseGameModel; + private hudText?: Text; + private graphLayer = new Container(); + + constructor() { + super("chase", SceneType.Normal); + } + + protected async onSceneLayout(): Promise { + this.stage.sortableChildren = true; + this.stage.eventMode = "passive"; + + this.hudText = new Text({ + text: "Chase: loading...", + style: new TextStyle({ + fontSize: 20, + fill: 0xffffff, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + this.hudText.position.set(24, 24); + this.stage.addChild(this.hudText); + + this.graphLayer.eventMode = "passive"; + this.graphLayer.position.set(140, 140); + this.stage.addChild(this.graphLayer); + + this.model = ChaseGameModel.createWithSeed({ + seed: 20260426, + difficulty: "normal", + }); + this.renderFromModel(); + } + + private renderFromModel(): void { + if (!this.model || !this.hudText) { + return; + } + + const state = this.model.getState(); + this.hudText.text = `Turn ${state.turn} | ${state.snapshot.status} | thief=${state.snapshot.thiefNodeId} guard=${state.snapshot.guardNodeId}`; + + this.graphLayer.removeChildren(); + const nodes = Object.values(state.snapshot.graph.nodes); + for (const node of nodes) { + const nodeView = new Graphics(); + nodeView.circle(0, 0, NODE_RADIUS); + const color = + node.id === state.snapshot.thiefNodeId + ? 0x22c55e + : node.id === state.snapshot.guardNodeId + ? 0xef4444 + : node.id === state.snapshot.exitNodeId + ? 0x3b82f6 + : 0x64748b; + nodeView.fill({ color, alpha: 0.95 }); + nodeView.stroke({ width: 2, color: 0xe2e8f0, alpha: 0.9 }); + nodeView.position.set(node.q * NODE_GAP, node.r * NODE_GAP); + nodeView.eventMode = "static"; + nodeView.cursor = "pointer"; + nodeView.on("pointerdown", () => this.handleNodeClick(node.id)); + this.graphLayer.addChild(nodeView); + } + } + + private handleNodeClick(targetNodeId: NodeId): void { + if (!this.model) { + return; + } + this.model.moveThief(targetNodeId); + this.renderFromModel(); + } +} diff --git a/src/stages/page_init.ts b/src/stages/page_init.ts index 2df0fff..5c2a3de 100644 --- a/src/stages/page_init.ts +++ b/src/stages/page_init.ts @@ -208,6 +208,6 @@ export default class InitScene extends BaseScene { this.startBtn.setDisabled(true); this.startBtn.setText("进入中..."); - void this.changeScene("welcome"); + void this.changeScene("chase"); } } diff --git a/tests/stages/page_chase.test.ts b/tests/stages/page_chase.test.ts new file mode 100644 index 0000000..4575f79 --- /dev/null +++ b/tests/stages/page_chase.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; +import ChaseScene from "../../src/stages/page_chase"; +import InitScene from "../../src/stages/page_init"; + +describe("chase stage skeleton", () => { + it("can instantiate chase scene with name=chase", () => { + const scene = new ChaseScene(); + expect(scene.name).toBe("chase"); + }); + + it("tries to call model.moveThief when clicking a graph node", () => { + const scene = new ChaseScene(); + const moveThief = vi.fn(); + (scene as any).model = { moveThief }; + + (scene as any).handleNodeClick("n-1"); + expect(moveThief).toHaveBeenCalledWith("n-1"); + }); + + it("init start entry routes to chase", () => { + const scene = new InitScene(); + const changeScene = vi.fn(); + const setDisabled = vi.fn(); + const setText = vi.fn(); + (scene as any).changeScene = changeScene; + (scene as any).startBtn = { setDisabled, setText }; + + (scene as any).handleStartClick(); + + expect(setDisabled).toHaveBeenCalledWith(true); + expect(setText).toHaveBeenCalledWith("进入中..."); + expect(changeScene).toHaveBeenCalledWith("chase"); + }); +});