Browse Source

feat(chase): add setup panel with difficulty selection and seed input

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
e3bb00f3c0
  1. 52
      src/stages/page_chase.ts
  2. 46
      tests/stages/page_chase.test.ts

52
src/stages/page_chase.ts

@ -1,13 +1,13 @@
import { BaseScene } from "@/scene/BaseScene"; import { BaseScene } from "@/scene/BaseScene";
import { SceneType } from "@/enums/SceneType"; import { SceneType } from "@/enums/SceneType";
import { ChaseGameModel } from "@/game/chase/model"; 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"; import { Container, Graphics, Text, TextStyle } from "pixi.js";
const NODE_RADIUS = 18; const NODE_RADIUS = 18;
const NODE_GAP = 58; const NODE_GAP = 58;
type ChaseModelLike = Pick<ChaseGameModel, "getState" | "moveThief">; type ChaseModelLike = Pick<ChaseGameModel, "getState" | "moveThief" | "newRound">;
type ChaseSceneOptions = { type ChaseSceneOptions = {
model?: ChaseModelLike; model?: ChaseModelLike;
modelFactory?: () => ChaseModelLike; modelFactory?: () => ChaseModelLike;
@ -18,7 +18,11 @@ export default class ChaseScene extends BaseScene {
private model?: ChaseModelLike; private model?: ChaseModelLike;
private readonly modelFactory?: () => ChaseModelLike; private readonly modelFactory?: () => ChaseModelLike;
private hudText?: Text; private hudText?: Text;
private setupText?: Text;
private graphLayer = new Container(); private graphLayer = new Container();
private selectedDifficulty: Difficulty = "normal";
private seedInput = "";
private difficultyLocked = false;
constructor(options: ChaseSceneOptions = {}) { constructor(options: ChaseSceneOptions = {}) {
super("chase", SceneType.Normal); super("chase", SceneType.Normal);
@ -41,6 +45,17 @@ export default class ChaseScene extends BaseScene {
this.hudText.position.set(24, 24); this.hudText.position.set(24, 24);
this.stage.addChild(this.hudText); 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.eventMode = "passive";
this.graphLayer.position.set(140, 140); this.graphLayer.position.set(140, 140);
this.stage.addChild(this.graphLayer); this.stage.addChild(this.graphLayer);
@ -57,12 +72,13 @@ export default class ChaseScene extends BaseScene {
} }
public refreshView(): void { public refreshView(): void {
if (!this.model || !this.hudText) { if (!this.model || !this.hudText || !this.setupText) {
return; return;
} }
const state = this.model.getState(); const state = this.model.getState();
this.hudText.text = `Turn ${state.turn} | ${state.snapshot.status} | thief=${state.snapshot.thiefNodeId} guard=${state.snapshot.guardNodeId}`; 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(); const oldChildren = this.graphLayer.removeChildren();
for (const child of oldChildren) { for (const child of oldChildren) {
@ -98,4 +114,34 @@ export default class ChaseScene extends BaseScene {
this.model.moveThief(targetNodeId); this.model.moveThief(targetNodeId);
this.refreshView(); 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();
}
} }

46
tests/stages/page_chase.test.ts

@ -58,6 +58,7 @@ describe("chase stage skeleton", () => {
}; };
const scene = new ChaseScene({ model: model as any }); const scene = new ChaseScene({ model: model as any });
(scene as any).hudText = new Text({ text: "" }); (scene as any).hudText = new Text({ text: "" });
(scene as any).setupText = new Text({ text: "" });
scene.refreshView(); scene.refreshView();
const firstChildren = [...(scene as any).graphLayer.children]; const firstChildren = [...(scene as any).graphLayer.children];
@ -85,4 +86,49 @@ describe("chase stage skeleton", () => {
expect(setText).toHaveBeenCalledWith("进入中..."); expect(setText).toHaveBeenCalledWith("进入中...");
expect(changeScene).toHaveBeenCalledWith("chase"); 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");
});
}); });

Loading…
Cancel
Save