diff --git a/src/stages/page_chase.ts b/src/stages/page_chase.ts index b2d8ec1..793e84e 100644 --- a/src/stages/page_chase.ts +++ b/src/stages/page_chase.ts @@ -7,14 +7,23 @@ import { Container, Graphics, Text, TextStyle } from "pixi.js"; const NODE_RADIUS = 18; const NODE_GAP = 58; +type ChaseModelLike = Pick; +type ChaseSceneOptions = { + model?: ChaseModelLike; + modelFactory?: () => ChaseModelLike; +}; + export default class ChaseScene extends BaseScene { stage = new Container(); - private model?: ChaseGameModel; + private model?: ChaseModelLike; + private readonly modelFactory?: () => ChaseModelLike; private hudText?: Text; private graphLayer = new Container(); - constructor() { + constructor(options: ChaseSceneOptions = {}) { super("chase", SceneType.Normal); + this.model = options.model; + this.modelFactory = options.modelFactory; } protected async onSceneLayout(): Promise { @@ -36,14 +45,18 @@ export default class ChaseScene extends BaseScene { this.graphLayer.position.set(140, 140); this.stage.addChild(this.graphLayer); - this.model = ChaseGameModel.createWithSeed({ - seed: 20260426, - difficulty: "normal", - }); - this.renderFromModel(); + if (!this.model) { + this.model = + this.modelFactory?.() ?? + ChaseGameModel.createWithSeed({ + seed: 20260426, + difficulty: "normal", + }); + } + this.refreshView(); } - private renderFromModel(): void { + public refreshView(): void { if (!this.model || !this.hudText) { return; } @@ -51,7 +64,11 @@ 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.graphLayer.removeChildren(); + const oldChildren = this.graphLayer.removeChildren(); + for (const child of oldChildren) { + child.destroy({ children: true }); + } + const nodes = Object.values(state.snapshot.graph.nodes); for (const node of nodes) { const nodeView = new Graphics(); @@ -79,6 +96,6 @@ export default class ChaseScene extends BaseScene { return; } this.model.moveThief(targetNodeId); - this.renderFromModel(); + this.refreshView(); } } diff --git a/tests/stages/page_chase.test.ts b/tests/stages/page_chase.test.ts index 4575f79..b56416d 100644 --- a/tests/stages/page_chase.test.ts +++ b/tests/stages/page_chase.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import ChaseScene from "../../src/stages/page_chase"; import InitScene from "../../src/stages/page_init"; +import { Text } from "pixi.js"; describe("chase stage skeleton", () => { it("can instantiate chase scene with name=chase", () => { @@ -17,6 +18,59 @@ describe("chase stage skeleton", () => { expect(moveThief).toHaveBeenCalledWith("n-1"); }); + it("cleans up old graph nodes when re-rendering", () => { + const model = { + moveThief: vi.fn(), + getState: vi + .fn() + .mockReturnValueOnce({ + 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"]], + }, + }, + }) + .mockReturnValueOnce({ + turn: 1, + snapshot: { + status: "playing", + thiefNodeId: "B", + guardNodeId: "A", + 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 scene = new ChaseScene({ model: model as any }); + (scene as any).hudText = new Text({ text: "" }); + + scene.refreshView(); + const firstChildren = [...(scene as any).graphLayer.children]; + expect(firstChildren.length).toBe(2); + + scene.refreshView(); + const secondChildren = [...(scene as any).graphLayer.children]; + expect(secondChildren.length).toBe(2); + expect(secondChildren[0]).not.toBe(firstChildren[0]); + expect((firstChildren[0] as any).destroyed).toBe(true); + expect((firstChildren[1] as any).destroyed).toBe(true); + }); + it("init start entry routes to chase", () => { const scene = new InitScene(); const changeScene = vi.fn();