Browse Source

feat(chase): wire interactive setup controls for seed and difficulty

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
fb8db007b1
  1. 127
      src/stages/page_chase.ts
  2. 66
      tests/stages/page_chase.test.ts

127
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<Difficulty, Graphics> = {
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();
};
}

66
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");
});
});

Loading…
Cancel
Save