|
|
@ -8,7 +8,10 @@ import { Container, Graphics, Text, TextStyle, type Ticker } 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" | "newRound">; |
|
|
type ChaseModelLike = Pick< |
|
|
|
|
|
ChaseGameModel, |
|
|
|
|
|
"getState" | "moveThief" | "newRound" | "retryRound" |
|
|
|
|
|
>; |
|
|
type DocumentLike = { |
|
|
type DocumentLike = { |
|
|
body: { |
|
|
body: { |
|
|
appendChild: (node: HTMLInputElement) => void; |
|
|
appendChild: (node: HTMLInputElement) => void; |
|
|
@ -30,12 +33,17 @@ export default class ChaseScene extends BaseScene { |
|
|
private setupText?: Text; |
|
|
private setupText?: Text; |
|
|
private setupLayer = new Container(); |
|
|
private setupLayer = new Container(); |
|
|
private graphLayer = new Container(); |
|
|
private graphLayer = new Container(); |
|
|
|
|
|
private resultOverlay = new Container(); |
|
|
|
|
|
private resultText = new Text({ text: "" }); |
|
|
private difficultyButtons: Record<Difficulty, Graphics> = { |
|
|
private difficultyButtons: Record<Difficulty, Graphics> = { |
|
|
easy: new Graphics(), |
|
|
easy: new Graphics(), |
|
|
normal: new Graphics(), |
|
|
normal: new Graphics(), |
|
|
hard: new Graphics(), |
|
|
hard: new Graphics(), |
|
|
}; |
|
|
}; |
|
|
private startButton = new Graphics(); |
|
|
private startButton = new Graphics(); |
|
|
|
|
|
private retryButton = new Graphics(); |
|
|
|
|
|
private newGameButton = new Graphics(); |
|
|
|
|
|
private restartButton = new Graphics(); |
|
|
private seedInputElement?: HTMLInputElement; |
|
|
private seedInputElement?: HTMLInputElement; |
|
|
private readonly documentRef?: DocumentLike; |
|
|
private readonly documentRef?: DocumentLike; |
|
|
private selectedDifficulty: Difficulty = "normal"; |
|
|
private selectedDifficulty: Difficulty = "normal"; |
|
|
@ -43,6 +51,7 @@ export default class ChaseScene extends BaseScene { |
|
|
private difficultyLocked = false; |
|
|
private difficultyLocked = false; |
|
|
private inputAttached = false; |
|
|
private inputAttached = false; |
|
|
private unsubscribeStageChange?: () => void; |
|
|
private unsubscribeStageChange?: () => void; |
|
|
|
|
|
private endActionLocked = false; |
|
|
|
|
|
|
|
|
constructor(options: ChaseSceneOptions = {}) { |
|
|
constructor(options: ChaseSceneOptions = {}) { |
|
|
super("chase", SceneType.Normal); |
|
|
super("chase", SceneType.Normal); |
|
|
@ -85,6 +94,8 @@ export default class ChaseScene extends BaseScene { |
|
|
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); |
|
|
|
|
|
this.buildResultOverlay(); |
|
|
|
|
|
this.stage.addChild(this.resultOverlay); |
|
|
|
|
|
|
|
|
if (!this.model) { |
|
|
if (!this.model) { |
|
|
this.model = |
|
|
this.model = |
|
|
@ -129,6 +140,7 @@ export default class ChaseScene extends BaseScene { |
|
|
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"}`; |
|
|
this.setupText.text = `difficulty=${this.selectedDifficulty} | seed=${this.seedInput.trim() || "(auto)"} | locked=${this.difficultyLocked ? "yes" : "no"}`; |
|
|
this.drawDifficultyButtons(); |
|
|
this.drawDifficultyButtons(); |
|
|
|
|
|
this.renderResultOverlay(state.snapshot.status, state.loseReason); |
|
|
|
|
|
|
|
|
const oldChildren = this.graphLayer.removeChildren(); |
|
|
const oldChildren = this.graphLayer.removeChildren(); |
|
|
for (const child of oldChildren) { |
|
|
for (const child of oldChildren) { |
|
|
@ -210,6 +222,33 @@ export default class ChaseScene extends BaseScene { |
|
|
this.refreshView(); |
|
|
this.refreshView(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private handleRetryClick(): void { |
|
|
|
|
|
if (!this.model || this.endActionLocked) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
this.endActionLocked = true; |
|
|
|
|
|
this.model.retryRound(); |
|
|
|
|
|
this.refreshView(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private handleNewGameClick(): void { |
|
|
|
|
|
if (!this.model || this.endActionLocked) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
this.endActionLocked = true; |
|
|
|
|
|
this.resetSetup(); |
|
|
|
|
|
this.startGame(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private handleRestartClick(): void { |
|
|
|
|
|
if (!this.model || this.endActionLocked) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
this.endActionLocked = true; |
|
|
|
|
|
this.resetSetup(); |
|
|
|
|
|
this.startGame(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private buildSetupControls(): void { |
|
|
private buildSetupControls(): void { |
|
|
this.setupLayer.removeChildren(); |
|
|
this.setupLayer.removeChildren(); |
|
|
const difficulties: Difficulty[] = ["easy", "normal", "hard"]; |
|
|
const difficulties: Difficulty[] = ["easy", "normal", "hard"]; |
|
|
@ -260,6 +299,66 @@ export default class ChaseScene extends BaseScene { |
|
|
this.startButton.fill({ color: 0x2563eb, alpha: 0.95 }); |
|
|
this.startButton.fill({ color: 0x2563eb, alpha: 0.95 }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private buildResultOverlay(): void { |
|
|
|
|
|
this.resultOverlay.visible = false; |
|
|
|
|
|
this.resultOverlay.eventMode = "passive"; |
|
|
|
|
|
this.resultOverlay.position.set(24, 170); |
|
|
|
|
|
|
|
|
|
|
|
this.resultText = new Text({ |
|
|
|
|
|
text: "", |
|
|
|
|
|
style: new TextStyle({ |
|
|
|
|
|
fontSize: 24, |
|
|
|
|
|
fill: 0xffffff, |
|
|
|
|
|
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|
|
|
|
|
}), |
|
|
|
|
|
}); |
|
|
|
|
|
this.resultOverlay.addChild(this.resultText); |
|
|
|
|
|
|
|
|
|
|
|
this.retryButton.eventMode = "static"; |
|
|
|
|
|
this.retryButton.cursor = "pointer"; |
|
|
|
|
|
this.retryButton.removeAllListeners(); |
|
|
|
|
|
this.retryButton.on("pointerdown", () => this.handleRetryClick()); |
|
|
|
|
|
this.retryButton.position.set(0, 42); |
|
|
|
|
|
this.retryButton.roundRect(0, 0, 86, 34, 8); |
|
|
|
|
|
this.retryButton.fill({ color: 0x2563eb, alpha: 0.95 }); |
|
|
|
|
|
this.resultOverlay.addChild(this.retryButton); |
|
|
|
|
|
this.resultOverlay.addChild(this.createOverlayLabel("重试", 43, 59)); |
|
|
|
|
|
|
|
|
|
|
|
this.newGameButton.eventMode = "static"; |
|
|
|
|
|
this.newGameButton.cursor = "pointer"; |
|
|
|
|
|
this.newGameButton.removeAllListeners(); |
|
|
|
|
|
this.newGameButton.on("pointerdown", () => this.handleNewGameClick()); |
|
|
|
|
|
this.newGameButton.position.set(98, 42); |
|
|
|
|
|
this.newGameButton.roundRect(0, 0, 96, 34, 8); |
|
|
|
|
|
this.newGameButton.fill({ color: 0x0ea5e9, alpha: 0.95 }); |
|
|
|
|
|
this.resultOverlay.addChild(this.newGameButton); |
|
|
|
|
|
this.resultOverlay.addChild(this.createOverlayLabel("新一局", 146, 59)); |
|
|
|
|
|
|
|
|
|
|
|
this.restartButton.eventMode = "static"; |
|
|
|
|
|
this.restartButton.cursor = "pointer"; |
|
|
|
|
|
this.restartButton.removeAllListeners(); |
|
|
|
|
|
this.restartButton.on("pointerdown", () => this.handleRestartClick()); |
|
|
|
|
|
this.restartButton.position.set(0, 42); |
|
|
|
|
|
this.restartButton.roundRect(0, 0, 108, 34, 8); |
|
|
|
|
|
this.restartButton.fill({ color: 0x22c55e, alpha: 0.95 }); |
|
|
|
|
|
this.resultOverlay.addChild(this.restartButton); |
|
|
|
|
|
this.resultOverlay.addChild(this.createOverlayLabel("重新开始", 54, 59)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private createOverlayLabel(text: string, x: number, y: number): Text { |
|
|
|
|
|
const label = new Text({ |
|
|
|
|
|
text, |
|
|
|
|
|
style: new TextStyle({ |
|
|
|
|
|
fontSize: 14, |
|
|
|
|
|
fill: 0xffffff, |
|
|
|
|
|
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", |
|
|
|
|
|
}), |
|
|
|
|
|
}); |
|
|
|
|
|
label.anchor.set(0.5); |
|
|
|
|
|
label.position.set(x, y); |
|
|
|
|
|
return label; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private drawDifficultyButtons(): void { |
|
|
private drawDifficultyButtons(): void { |
|
|
const drawButton = (difficulty: Difficulty): void => { |
|
|
const drawButton = (difficulty: Difficulty): void => { |
|
|
const button = this.difficultyButtons[difficulty]; |
|
|
const button = this.difficultyButtons[difficulty]; |
|
|
@ -333,4 +432,29 @@ export default class ChaseScene extends BaseScene { |
|
|
} |
|
|
} |
|
|
return undefined; |
|
|
return undefined; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private renderResultOverlay( |
|
|
|
|
|
status: "playing" | "win" | "lose", |
|
|
|
|
|
loseReason?: "caught" | "trapped", |
|
|
|
|
|
): void { |
|
|
|
|
|
if (status === "playing") { |
|
|
|
|
|
this.resultOverlay.visible = false; |
|
|
|
|
|
this.endActionLocked = false; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.resultOverlay.visible = true; |
|
|
|
|
|
if (status === "win") { |
|
|
|
|
|
this.resultText.text = "成功逃脱!"; |
|
|
|
|
|
this.restartButton.visible = true; |
|
|
|
|
|
this.retryButton.visible = false; |
|
|
|
|
|
this.newGameButton.visible = false; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.resultText.text = loseReason === "trapped" ? "无路可走" : "被抓住"; |
|
|
|
|
|
this.restartButton.visible = false; |
|
|
|
|
|
this.retryButton.visible = true; |
|
|
|
|
|
this.newGameButton.visible = true; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|