From fb8db007b13e4bfaab1ea6c0f0ee77259dcf9c53 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 26 Apr 2026 23:52:08 +0800 Subject: [PATCH] feat(chase): wire interactive setup controls for seed and difficulty Made-with: Cursor --- src/stages/page_chase.ts | 127 ++++++++++++++++++++++++++++++++++++++++ tests/stages/page_chase.test.ts | 66 +++++++++++---------- 2 files changed, 162 insertions(+), 31 deletions(-) diff --git a/src/stages/page_chase.ts b/src/stages/page_chase.ts index 25db7a5..bb90513 100644 --- a/src/stages/page_chase.ts +++ b/src/stages/page_chase.ts @@ -19,7 +19,15 @@ export default class ChaseScene extends BaseScene { private readonly modelFactory?: () => ChaseModelLike; private hudText?: Text; private setupText?: Text; + private setupLayer = new Container(); private graphLayer = new Container(); + private difficultyButtons: Record = { + easy: new Graphics(), + normal: new Graphics(), + hard: new Graphics(), + }; + private startButton = new Graphics(); + private seedInputElement?: HTMLInputElement; private selectedDifficulty: Difficulty = "normal"; private seedInput = ""; private difficultyLocked = false; @@ -56,6 +64,11 @@ export default class ChaseScene extends BaseScene { this.setupText.position.set(24, 54); this.stage.addChild(this.setupText); + this.setupLayer.eventMode = "passive"; + this.setupLayer.position.set(24, 84); + this.stage.addChild(this.setupLayer); + this.buildSetupControls(); + this.graphLayer.eventMode = "passive"; this.graphLayer.position.set(140, 140); this.stage.addChild(this.graphLayer); @@ -71,6 +84,14 @@ export default class ChaseScene extends BaseScene { this.refreshView(); } + protected onSceneEnter(): void { + this.attachSeedInputBridge(); + } + + protected onSceneExit(): void { + this.detachSeedInputBridge(); + } + public refreshView(): void { if (!this.model || !this.hudText || !this.setupText) { return; @@ -79,6 +100,7 @@ export default class ChaseScene extends BaseScene { 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"}`; + this.drawDifficultyButtons(); const oldChildren = this.graphLayer.removeChildren(); for (const child of oldChildren) { @@ -125,6 +147,9 @@ export default class ChaseScene extends BaseScene { public setSeedInput(seedInput: string): void { this.seedInput = seedInput; + if (this.seedInputElement) { + this.seedInputElement.value = seedInput; + } this.refreshView(); } @@ -142,6 +167,108 @@ export default class ChaseScene extends BaseScene { this.model.newRound(seed, this.selectedDifficulty); this.difficultyLocked = true; this.seedInput = `${seed}`; + if (this.seedInputElement) { + this.seedInputElement.value = this.seedInput; + } this.refreshView(); } + + private buildSetupControls(): void { + this.setupLayer.removeChildren(); + const difficulties: Difficulty[] = ["easy", "normal", "hard"]; + difficulties.forEach((difficulty, index) => { + const button = this.difficultyButtons[difficulty]; + button.eventMode = "static"; + button.cursor = "pointer"; + button.removeAllListeners(); + button.on("pointerdown", () => this.setDifficulty(difficulty)); + button.position.set(index * 96, 0); + this.setupLayer.addChild(button); + + const label = new Text({ + text: difficulty, + style: new TextStyle({ + fontSize: 14, + fill: 0xffffff, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + label.anchor.set(0.5); + label.position.set(index * 96 + 38, 18); + this.setupLayer.addChild(label); + }); + + this.startButton.eventMode = "static"; + this.startButton.cursor = "pointer"; + this.startButton.removeAllListeners(); + this.startButton.on("pointerdown", () => this.startGame()); + this.startButton.position.set(304, 0); + this.setupLayer.addChild(this.startButton); + + const startLabel = new Text({ + text: "开始游戏", + style: new TextStyle({ + fontSize: 14, + fill: 0xffffff, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + startLabel.anchor.set(0.5); + startLabel.position.set(304 + 48, 18); + this.setupLayer.addChild(startLabel); + + this.drawDifficultyButtons(); + this.startButton.clear(); + this.startButton.roundRect(0, 0, 96, 36, 8); + this.startButton.fill({ color: 0x2563eb, alpha: 0.95 }); + } + + private drawDifficultyButtons(): void { + const drawButton = (difficulty: Difficulty): void => { + const button = this.difficultyButtons[difficulty]; + button.clear(); + button.roundRect(0, 0, 76, 36, 8); + const selected = this.selectedDifficulty === difficulty; + const lockedColor = this.difficultyLocked ? 0x475569 : selected ? 0x0ea5e9 : 0x1f2937; + button.fill({ color: lockedColor, alpha: selected ? 0.95 : 0.88 }); + button.stroke({ width: selected ? 2 : 1, color: 0xe2e8f0, alpha: 0.9 }); + button.cursor = this.difficultyLocked ? "not-allowed" : "pointer"; + }; + drawButton("easy"); + drawButton("normal"); + drawButton("hard"); + } + + private attachSeedInputBridge(): void { + if (typeof document === "undefined" || this.seedInputElement) { + return; + } + const input = document.createElement("input"); + input.type = "text"; + input.placeholder = "Seed (optional)"; + input.value = this.seedInput; + input.style.position = "fixed"; + input.style.left = "24px"; + input.style.top = "130px"; + input.style.zIndex = "10"; + input.style.width = "180px"; + input.addEventListener("input", this.handleSeedInputEvent); + document.body.appendChild(input); + this.seedInputElement = input; + } + + private detachSeedInputBridge(): void { + if (!this.seedInputElement) { + return; + } + this.seedInputElement.removeEventListener("input", this.handleSeedInputEvent); + this.seedInputElement.remove(); + this.seedInputElement = undefined; + } + + private readonly handleSeedInputEvent = (event: Event): void => { + const target = event.target as HTMLInputElement | null; + this.seedInput = target?.value ?? ""; + this.refreshView(); + }; } diff --git a/tests/stages/page_chase.test.ts b/tests/stages/page_chase.test.ts index cd4ba4b..6aef980 100644 --- a/tests/stages/page_chase.test.ts +++ b/tests/stages/page_chase.test.ts @@ -3,6 +3,25 @@ import ChaseScene from "../../src/stages/page_chase"; import InitScene from "../../src/stages/page_init"; import { Text } from "pixi.js"; +function createSceneState() { + return { + turn: 0, + snapshot: { + status: "playing", + thiefNodeId: "A", + guardNodeId: "B", + exitNodeId: "B", + graph: { + nodes: { + A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, + B: { id: "B", q: 1, r: 0, neighbors: ["A"] }, + }, + edgeList: [["A", "B"]], + }, + }, + }; +} + describe("chase stage skeleton", () => { it("can instantiate chase scene with name=chase", () => { const scene = new ChaseScene(); @@ -89,14 +108,15 @@ describe("chase stage skeleton", () => { it("uses manual seed and difficulty when starting game", () => { const newRound = vi.fn(); + const getState = vi.fn().mockReturnValue(createSceneState()); const scene = new ChaseScene({ - model: { getState: vi.fn(), moveThief: vi.fn(), newRound } as any, + model: { getState, moveThief: vi.fn(), newRound } as any, }); - (scene as any).hudText = new Text({ text: "" }); - (scene as any).setDifficulty("hard"); + void (scene as any).onSceneLayout(); (scene as any).setSeedInput("4242"); - - (scene as any).startGame(); + const hardBtn = (scene as any).difficultyButtons.hard; + hardBtn.emit("pointerdown"); + (scene as any).startButton.emit("pointerdown"); expect(newRound).toHaveBeenCalledWith(4242, "hard"); }); @@ -118,15 +138,15 @@ describe("chase stage skeleton", () => { it("locks difficulty after start and ignores later changes", () => { const newRound = vi.fn(); + const getState = vi.fn().mockReturnValue(createSceneState()); const scene = new ChaseScene({ - model: { getState: vi.fn(), moveThief: vi.fn(), newRound } as any, + model: { getState, 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(); + void (scene as any).onSceneLayout(); + (scene as any).difficultyButtons.easy.emit("pointerdown"); + (scene as any).startButton.emit("pointerdown"); + (scene as any).difficultyButtons.hard.emit("pointerdown"); + (scene as any).startButton.emit("pointerdown"); expect(newRound.mock.calls[0][1]).toBe("easy"); expect(newRound.mock.calls[1][1]).toBe("easy"); @@ -134,30 +154,14 @@ describe("chase stage skeleton", () => { it("uses normal as default difficulty", () => { const newRound = vi.fn(); - const getState = vi.fn().mockReturnValue({ - turn: 0, - snapshot: { - status: "playing", - thiefNodeId: "A", - guardNodeId: "B", - exitNodeId: "B", - graph: { - nodes: { - A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, - B: { id: "B", q: 1, r: 0, neighbors: ["A"] }, - }, - edgeList: [["A", "B"]], - }, - }, - }); + const getState = vi.fn().mockReturnValue(createSceneState()); const scene = new ChaseScene({ model: { getState, moveThief: vi.fn(), newRound } as any, }); - (scene as any).hudText = new Text({ text: "" }); - (scene as any).setupText = new Text({ text: "" }); + void (scene as any).onSceneLayout(); (scene as any).setSeedInput("1001"); - (scene as any).startGame(); + (scene as any).startButton.emit("pointerdown"); expect(newRound).toHaveBeenCalledWith(1001, "normal"); }); });