diff --git a/src/stages/page_chase.ts b/src/stages/page_chase.ts index f9970b8..d1088e4 100644 --- a/src/stages/page_chase.ts +++ b/src/stages/page_chase.ts @@ -31,6 +31,8 @@ export default class ChaseScene extends BaseScene { private model?: ChaseModelLike; private readonly modelFactory?: () => ChaseModelLike; private hudText?: Text; + private winCountText?: Text; + private seedText?: Text; private setupText?: Text; private setupLayer = new Container(); private graphLayer = new Container(); @@ -80,6 +82,29 @@ export default class ChaseScene extends BaseScene { this.hudText.position.set(24, 24); this.stage.addChild(this.hudText); + this.winCountText = new Text({ + text: "", + style: new TextStyle({ + fontSize: 18, + fill: 0xffffff, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + this.winCountText.position.set(24, 6); + this.stage.addChild(this.winCountText); + + this.seedText = new Text({ + text: "", + style: new TextStyle({ + fontSize: 18, + fill: 0xffffff, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + this.seedText.anchor.set(1, 0); + this.seedText.position.set(760, 6); + this.stage.addChild(this.seedText); + this.setupText = new Text({ text: "", style: new TextStyle({ @@ -137,11 +162,13 @@ export default class ChaseScene extends BaseScene { } public refreshView(): void { - if (!this.model || !this.hudText || !this.setupText) { + if (!this.model || !this.hudText || !this.setupText || !this.winCountText || !this.seedText) { return; } const state = this.model.getState(); + this.winCountText.text = `成功次数: ${state.winCount}`; + this.seedText.text = `Seed: ${state.snapshot.seed}`; this.hudText.text = `Turn ${state.turn} | ${state.snapshot.status} | thief=${state.snapshot.thiefNodeId} guard=${state.snapshot.guardNodeId}`; this.setupText.text = `difficulty=${this.selectedDifficulty} | seed=${this.seedInput.trim() || "(auto)"} | locked=${this.difficultyLocked ? "yes" : "no"}`; this.drawDifficultyButtons(); @@ -152,6 +179,23 @@ export default class ChaseScene extends BaseScene { child.destroy({ children: true }); } + const thiefNode = state.snapshot.graph.nodes[state.snapshot.thiefNodeId]; + const availableMoves = thiefNode + ? thiefNode.neighbors.filter((id) => id !== state.snapshot.guardNodeId) + : []; + for (const nodeId of availableMoves) { + const node = state.snapshot.graph.nodes[nodeId]; + if (!node) { + continue; + } + const highlight = new Graphics(); + highlight.label = "move-highlight"; + highlight.circle(0, 0, NODE_RADIUS + 8); + highlight.stroke({ width: 3, color: 0xfacc15, alpha: 0.9 }); + highlight.position.set(node.q * NODE_GAP, node.r * NODE_GAP); + this.graphLayer.addChild(highlight); + } + const nodes = Object.values(state.snapshot.graph.nodes); for (const node of nodes) { const nodeView = new Graphics(); diff --git a/tests/stages/page_chase.test.ts b/tests/stages/page_chase.test.ts index d14c3bf..6d4f87b 100644 --- a/tests/stages/page_chase.test.ts +++ b/tests/stages/page_chase.test.ts @@ -7,7 +7,9 @@ import { Text } from "pixi.js"; function createSceneState() { return { turn: 0, + winCount: 0, snapshot: { + seed: 123, status: "playing", thiefNodeId: "A", guardNodeId: "B", @@ -110,6 +112,8 @@ describe("chase stage skeleton", () => { }; const scene = new ChaseScene({ model: model as any }); (scene as any).hudText = new Text({ text: "" }); + (scene as any).winCountText = new Text({ text: "" }); + (scene as any).seedText = new Text({ text: "" }); (scene as any).setupText = new Text({ text: "" }); scene.refreshView(); @@ -379,4 +383,67 @@ describe("chase stage skeleton", () => { void (scene as any).onSceneLayout(); expect((scene as any).resultText.text).toContain("你被抓住了"); }); + + it("shows hud with success counter and current seed", () => { + const scene = new ChaseScene({ + model: { + getState: vi.fn().mockReturnValue(createSceneState()), + moveThief: vi.fn(), + newRound: vi.fn(), + retryRound: vi.fn(), + } as any, + }); + void (scene as any).onSceneLayout(); + expect((scene as any).winCountText.text).toContain("成功次数: 0"); + expect((scene as any).seedText.text).toContain("Seed: 123"); + }); + + it("refreshes hud after state change", () => { + const getState = vi + .fn() + .mockReturnValueOnce(createSceneState()) + .mockReturnValueOnce({ + ...createSceneState(), + winCount: 2, + snapshot: { + ...createSceneState().snapshot, + seed: 999, + thiefNodeId: "B", + guardNodeId: "A", + }, + }); + const scene = new ChaseScene({ + model: { + getState, + moveThief: vi.fn(), + newRound: vi.fn(), + retryRound: vi.fn(), + } as any, + }); + void (scene as any).onSceneLayout(); + scene.refreshView(); + expect((scene as any).winCountText.text).toContain("成功次数: 2"); + expect((scene as any).seedText.text).toContain("Seed: 999"); + }); + + it("renders move highlights matching available moves count", () => { + const state = createSceneState(); + const scene = new ChaseScene({ + model: { + getState: vi.fn().mockReturnValue(state), + moveThief: vi.fn(), + newRound: vi.fn(), + retryRound: vi.fn(), + } as any, + }); + void (scene as any).onSceneLayout(); + const highlightCount = (scene as any).graphLayer.children.filter( + (c: any) => c.label === "move-highlight", + ).length; + const thiefNode = state.snapshot.graph.nodes[state.snapshot.thiefNodeId]; + const expectedMoves = thiefNode.neighbors.filter( + (id: string) => id !== state.snapshot.guardNodeId, + ).length; + expect(highlightCount).toBe(expectedMoves); + }); });