From e3bb00f3c03b7de2424c90828c8f189b7035cd51 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 26 Apr 2026 23:48:50 +0800 Subject: [PATCH] feat(chase): add setup panel with difficulty selection and seed input Made-with: Cursor --- src/stages/page_chase.ts | 52 ++++++++++++++++++++++++++++++++++++++--- tests/stages/page_chase.test.ts | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/stages/page_chase.ts b/src/stages/page_chase.ts index 793e84e..25db7a5 100644 --- a/src/stages/page_chase.ts +++ b/src/stages/page_chase.ts @@ -1,13 +1,13 @@ import { BaseScene } from "@/scene/BaseScene"; import { SceneType } from "@/enums/SceneType"; import { ChaseGameModel } from "@/game/chase/model"; -import type { NodeId } from "@/game/chase/types"; +import type { Difficulty, NodeId } from "@/game/chase/types"; import { Container, Graphics, Text, TextStyle } from "pixi.js"; const NODE_RADIUS = 18; const NODE_GAP = 58; -type ChaseModelLike = Pick; +type ChaseModelLike = Pick; type ChaseSceneOptions = { model?: ChaseModelLike; modelFactory?: () => ChaseModelLike; @@ -18,7 +18,11 @@ export default class ChaseScene extends BaseScene { private model?: ChaseModelLike; private readonly modelFactory?: () => ChaseModelLike; private hudText?: Text; + private setupText?: Text; private graphLayer = new Container(); + private selectedDifficulty: Difficulty = "normal"; + private seedInput = ""; + private difficultyLocked = false; constructor(options: ChaseSceneOptions = {}) { super("chase", SceneType.Normal); @@ -41,6 +45,17 @@ export default class ChaseScene extends BaseScene { this.hudText.position.set(24, 24); this.stage.addChild(this.hudText); + this.setupText = new Text({ + text: "", + style: new TextStyle({ + fontSize: 16, + fill: 0xdbeafe, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + this.setupText.position.set(24, 54); + this.stage.addChild(this.setupText); + this.graphLayer.eventMode = "passive"; this.graphLayer.position.set(140, 140); this.stage.addChild(this.graphLayer); @@ -57,12 +72,13 @@ export default class ChaseScene extends BaseScene { } public refreshView(): void { - if (!this.model || !this.hudText) { + if (!this.model || !this.hudText || !this.setupText) { return; } const state = this.model.getState(); 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"}`; const oldChildren = this.graphLayer.removeChildren(); for (const child of oldChildren) { @@ -98,4 +114,34 @@ export default class ChaseScene extends BaseScene { this.model.moveThief(targetNodeId); this.refreshView(); } + + public setDifficulty(difficulty: Difficulty): void { + if (this.difficultyLocked) { + return; + } + this.selectedDifficulty = difficulty; + this.refreshView(); + } + + public setSeedInput(seedInput: string): void { + this.seedInput = seedInput; + this.refreshView(); + } + + public startGame(): void { + if (!this.model) { + return; + } + const seedText = this.seedInput.trim(); + const parsed = Number(seedText); + const seed = + seedText.length === 0 || !Number.isFinite(parsed) + ? Math.floor(Math.random() * 0xffffffff) + : parsed; + + this.model.newRound(seed, this.selectedDifficulty); + this.difficultyLocked = true; + this.seedInput = `${seed}`; + this.refreshView(); + } } diff --git a/tests/stages/page_chase.test.ts b/tests/stages/page_chase.test.ts index b56416d..7a32fad 100644 --- a/tests/stages/page_chase.test.ts +++ b/tests/stages/page_chase.test.ts @@ -58,6 +58,7 @@ describe("chase stage skeleton", () => { }; const scene = new ChaseScene({ model: model as any }); (scene as any).hudText = new Text({ text: "" }); + (scene as any).setupText = new Text({ text: "" }); scene.refreshView(); const firstChildren = [...(scene as any).graphLayer.children]; @@ -85,4 +86,49 @@ describe("chase stage skeleton", () => { expect(setText).toHaveBeenCalledWith("进入中..."); expect(changeScene).toHaveBeenCalledWith("chase"); }); + + it("uses manual seed and difficulty when starting game", () => { + const newRound = vi.fn(); + const scene = new ChaseScene({ + model: { getState: vi.fn(), moveThief: vi.fn(), newRound } as any, + }); + (scene as any).hudText = new Text({ text: "" }); + (scene as any).setDifficulty("hard"); + (scene as any).setSeedInput("4242"); + + (scene as any).startGame(); + expect(newRound).toHaveBeenCalledWith(4242, "hard"); + }); + + it("auto-generates numeric seed when input is empty", () => { + const newRound = vi.fn(); + const scene = new ChaseScene({ + model: { getState: vi.fn(), moveThief: vi.fn(), newRound } as any, + }); + (scene as any).hudText = new Text({ text: "" }); + (scene as any).setSeedInput(" "); + (scene as any).setDifficulty("easy"); + + (scene as any).startGame(); + const seedArg = newRound.mock.calls[0][0]; + expect(typeof seedArg).toBe("number"); + expect(Number.isFinite(seedArg)).toBe(true); + expect(newRound.mock.calls[0][1]).toBe("easy"); + }); + + it("locks difficulty after start and ignores later changes", () => { + const newRound = vi.fn(); + const scene = new ChaseScene({ + model: { getState: vi.fn(), moveThief: vi.fn(), newRound } as any, + }); + (scene as any).hudText = new Text({ text: "" }); + (scene as any).setDifficulty("easy"); + + (scene as any).startGame(); + (scene as any).setDifficulty("hard"); + (scene as any).startGame(); + + expect(newRound.mock.calls[0][1]).toBe("easy"); + expect(newRound.mock.calls[1][1]).toBe("easy"); + }); });