3 changed files with 119 additions and 1 deletions
@ -0,0 +1,84 @@ |
|||
import { BaseScene } from "@/scene/BaseScene"; |
|||
import { SceneType } from "@/enums/SceneType"; |
|||
import { ChaseGameModel } from "@/game/chase/model"; |
|||
import type { NodeId } from "@/game/chase/types"; |
|||
import { Container, Graphics, Text, TextStyle } from "pixi.js"; |
|||
|
|||
const NODE_RADIUS = 18; |
|||
const NODE_GAP = 58; |
|||
|
|||
export default class ChaseScene extends BaseScene { |
|||
stage = new Container(); |
|||
private model?: ChaseGameModel; |
|||
private hudText?: Text; |
|||
private graphLayer = new Container(); |
|||
|
|||
constructor() { |
|||
super("chase", SceneType.Normal); |
|||
} |
|||
|
|||
protected async onSceneLayout(): Promise<void> { |
|||
this.stage.sortableChildren = true; |
|||
this.stage.eventMode = "passive"; |
|||
|
|||
this.hudText = new Text({ |
|||
text: "Chase: loading...", |
|||
style: new TextStyle({ |
|||
fontSize: 20, |
|||
fill: 0xffffff, |
|||
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|||
}), |
|||
}); |
|||
this.hudText.position.set(24, 24); |
|||
this.stage.addChild(this.hudText); |
|||
|
|||
this.graphLayer.eventMode = "passive"; |
|||
this.graphLayer.position.set(140, 140); |
|||
this.stage.addChild(this.graphLayer); |
|||
|
|||
this.model = ChaseGameModel.createWithSeed({ |
|||
seed: 20260426, |
|||
difficulty: "normal", |
|||
}); |
|||
this.renderFromModel(); |
|||
} |
|||
|
|||
private renderFromModel(): void { |
|||
if (!this.model || !this.hudText) { |
|||
return; |
|||
} |
|||
|
|||
const state = this.model.getState(); |
|||
this.hudText.text = `Turn ${state.turn} | ${state.snapshot.status} | thief=${state.snapshot.thiefNodeId} guard=${state.snapshot.guardNodeId}`; |
|||
|
|||
this.graphLayer.removeChildren(); |
|||
const nodes = Object.values(state.snapshot.graph.nodes); |
|||
for (const node of nodes) { |
|||
const nodeView = new Graphics(); |
|||
nodeView.circle(0, 0, NODE_RADIUS); |
|||
const color = |
|||
node.id === state.snapshot.thiefNodeId |
|||
? 0x22c55e |
|||
: node.id === state.snapshot.guardNodeId |
|||
? 0xef4444 |
|||
: node.id === state.snapshot.exitNodeId |
|||
? 0x3b82f6 |
|||
: 0x64748b; |
|||
nodeView.fill({ color, alpha: 0.95 }); |
|||
nodeView.stroke({ width: 2, color: 0xe2e8f0, alpha: 0.9 }); |
|||
nodeView.position.set(node.q * NODE_GAP, node.r * NODE_GAP); |
|||
nodeView.eventMode = "static"; |
|||
nodeView.cursor = "pointer"; |
|||
nodeView.on("pointerdown", () => this.handleNodeClick(node.id)); |
|||
this.graphLayer.addChild(nodeView); |
|||
} |
|||
} |
|||
|
|||
private handleNodeClick(targetNodeId: NodeId): void { |
|||
if (!this.model) { |
|||
return; |
|||
} |
|||
this.model.moveThief(targetNodeId); |
|||
this.renderFromModel(); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
import { describe, expect, it, vi } from "vitest"; |
|||
import ChaseScene from "../../src/stages/page_chase"; |
|||
import InitScene from "../../src/stages/page_init"; |
|||
|
|||
describe("chase stage skeleton", () => { |
|||
it("can instantiate chase scene with name=chase", () => { |
|||
const scene = new ChaseScene(); |
|||
expect(scene.name).toBe("chase"); |
|||
}); |
|||
|
|||
it("tries to call model.moveThief when clicking a graph node", () => { |
|||
const scene = new ChaseScene(); |
|||
const moveThief = vi.fn(); |
|||
(scene as any).model = { moveThief }; |
|||
|
|||
(scene as any).handleNodeClick("n-1"); |
|||
expect(moveThief).toHaveBeenCalledWith("n-1"); |
|||
}); |
|||
|
|||
it("init start entry routes to chase", () => { |
|||
const scene = new InitScene(); |
|||
const changeScene = vi.fn(); |
|||
const setDisabled = vi.fn(); |
|||
const setText = vi.fn(); |
|||
(scene as any).changeScene = changeScene; |
|||
(scene as any).startBtn = { setDisabled, setText }; |
|||
|
|||
(scene as any).handleStartClick(); |
|||
|
|||
expect(setDisabled).toHaveBeenCalledWith(true); |
|||
expect(setText).toHaveBeenCalledWith("进入中..."); |
|||
expect(changeScene).toHaveBeenCalledWith("chase"); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue