diff --git a/src/stages/page_chase.ts b/src/stages/page_chase.ts index ff7a2a9..36f20b4 100644 --- a/src/stages/page_chase.ts +++ b/src/stages/page_chase.ts @@ -1,6 +1,7 @@ import { BaseScene } from "@/scene/BaseScene"; import { sceneManager } from "@/scene/SceneManager"; import { SceneType } from "@/enums/SceneType"; +import { appRuntime } from "@/kernel/AppRuntime"; import { ChaseGameModel } from "@/game/chase/model"; import type { Difficulty, NodeId } from "@/game/chase/types"; import { Container, Graphics, Text, TextStyle, type Ticker } from "pixi.js"; @@ -8,6 +9,11 @@ import { Container, Graphics, Text, TextStyle, type Ticker } from "pixi.js"; const NODE_RADIUS = 18; const NODE_GAP = 58; const END_ACTION_COOLDOWN_MS = 400; +const PAD_X = 20; +const PAD_Y = 12; +/** 顶栏 + 开局控件占用高度(用于把地图放到下方安全区) */ +const TOP_CHROME_H = 168; +const SEED_INPUT_WIDTH = 220; type ChaseModelLike = Pick< ChaseGameModel, @@ -59,6 +65,9 @@ export default class ChaseScene extends BaseScene { private unsubscribeStageChange?: () => void; private endActionLocked = false; private lastEndActionAt = 0; + private readonly onWindowResize = (): void => { + this.relayoutChrome(); + }; constructor(options: ChaseSceneOptions = {}) { super("chase", SceneType.Normal); @@ -74,59 +83,54 @@ export default class ChaseScene extends BaseScene { this.hudText = new Text({ text: "Chase: loading...", style: new TextStyle({ - fontSize: 20, - fill: 0xffffff, + fontSize: 16, + fill: 0xe2e8f0, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", }), }); - this.hudText.position.set(24, 24); this.stage.addChild(this.hudText); this.winCountText = new Text({ text: "", style: new TextStyle({ - fontSize: 18, + fontSize: 17, fill: 0xffffff, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", }), }); - this.winCountText.position.set(24, 6); this.stage.addChild(this.winCountText); this.seedText = new Text({ text: "", style: new TextStyle({ - fontSize: 18, + fontSize: 17, fill: 0xffffff, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", }), }); this.seedText.anchor.set(1, 0); - this.seedText.position.set(760, 6); this.stage.addChild(this.seedText); this.setupText = new Text({ text: "", style: new TextStyle({ - fontSize: 16, - fill: 0xdbeafe, + fontSize: 14, + fill: 0x93c5fd, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", }), }); - 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); this.buildResultOverlay(); this.stage.addChild(this.resultOverlay); + this.relayoutChrome(); if (!this.model) { this.model = this.modelFactory?.() ?? @@ -139,6 +143,9 @@ export default class ChaseScene extends BaseScene { } protected onSceneEnter(): void { + if (typeof globalThis !== "undefined" && "addEventListener" in globalThis) { + globalThis.addEventListener("resize", this.onWindowResize); + } if (!this.unsubscribeStageChange) { this.unsubscribeStageChange = sceneManager.onStageChange((current) => { if (current.name !== this.name) { @@ -152,6 +159,9 @@ export default class ChaseScene extends BaseScene { } protected onSceneExit(): void { + if (typeof globalThis !== "undefined" && "removeEventListener" in globalThis) { + globalThis.removeEventListener("resize", this.onWindowResize); + } this.unsubscribeStageChange?.(); this.unsubscribeStageChange = undefined; this.detachSeedInputBridge(); @@ -179,6 +189,9 @@ export default class ChaseScene extends BaseScene { child.destroy({ children: true }); } + const nodes = Object.values(state.snapshot.graph.nodes); + const { offsetX, offsetY } = this.computeGraphCenterOffset(nodes); + const thiefNode = state.snapshot.graph.nodes[state.snapshot.thiefNodeId]; const availableMoves = thiefNode ? thiefNode.neighbors.filter((id) => id !== state.snapshot.guardNodeId) @@ -192,11 +205,10 @@ export default class ChaseScene extends BaseScene { highlight.label = "move-highlight"; highlight.circle(0, 0, NODE_RADIUS + 8); highlight.stroke({ width: 3, color: 0xfacc15, alpha: 0.9 }); - highlight.position.set(node.q * NODE_GAP, node.r * NODE_GAP); + highlight.position.set(node.q * NODE_GAP + offsetX, node.r * NODE_GAP + offsetY); this.graphLayer.addChild(highlight); } - const nodes = Object.values(state.snapshot.graph.nodes); for (const node of nodes) { const nodeView = new Graphics(); nodeView.circle(0, 0, NODE_RADIUS); @@ -210,7 +222,7 @@ export default class ChaseScene extends BaseScene { : 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.position.set(node.q * NODE_GAP + offsetX, node.r * NODE_GAP + offsetY); nodeView.eventMode = "static"; nodeView.cursor = "pointer"; nodeView.on("pointerdown", () => this.handleNodeClick(node.id)); @@ -218,6 +230,58 @@ export default class ChaseScene extends BaseScene { } } + /** 将节点包络中心移到局部原点,避免负坐标画进顶栏 */ + private computeGraphCenterOffset( + nodes: Array<{ q: number; r: number }>, + ): { offsetX: number; offsetY: number } { + if (nodes.length === 0) { + return { offsetX: 0, offsetY: 0 }; + } + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + for (const node of nodes) { + const x = node.q * NODE_GAP; + const y = node.r * NODE_GAP; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + const cx = (minX + maxX) / 2; + const cy = (minY + maxY) / 2; + return { offsetX: -cx, offsetY: -cy }; + } + + private relayoutChrome(): void { + const { width, height } = appRuntime.game.getInfo(); + const safeTop = PAD_Y; + this.winCountText?.position.set(PAD_X, safeTop); + this.seedText?.position.set(width - PAD_X, safeTop); + this.hudText?.position.set(PAD_X, safeTop + 26); + this.setupText?.position.set(PAD_X, safeTop + 52); + this.setupLayer.position.set(PAD_X, safeTop + 78); + + const playTop = Math.min(TOP_CHROME_H, height * 0.28); + const playCenterY = playTop + (height - playTop) / 2; + this.graphLayer.position.set(width / 2, playCenterY); + + this.resultOverlay.position.set(width / 2 - 120, playTop + 8); + + this.positionSeedInputDom(); + } + + private positionSeedInputDom(): void { + if (!this.seedInputElement) { + return; + } + const topPx = this.setupLayer.position.y + 48; + this.seedInputElement.style.left = `${PAD_X}px`; + this.seedInputElement.style.top = `${topPx}px`; + this.seedInputElement.style.width = `${SEED_INPUT_WIDTH}px`; + } + private handleNodeClick(targetNodeId: NodeId): void { if (!this.model) { return; @@ -363,7 +427,6 @@ export default class ChaseScene extends BaseScene { private buildResultOverlay(): void { this.resultOverlay.visible = false; this.resultOverlay.eventMode = "passive"; - this.resultOverlay.position.set(24, 170); this.resultText = new Text({ text: "", @@ -449,14 +512,17 @@ export default class ChaseScene extends BaseScene { 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.style.boxSizing = "border-box"; + input.style.padding = "6px 10px"; + input.style.fontSize = "14px"; + input.style.borderRadius = "6px"; + input.style.border = "1px solid #94a3b8"; input.addEventListener("input", this.handleSeedInputEvent); doc.body.appendChild(input); this.seedInputElement = input; this.inputAttached = true; + this.positionSeedInputDom(); } private detachSeedInputBridge(): void {