Browse Source

fix(chase): center graph and relayout HUD to stop UI overlap

Hex nodes used raw axial coordinates so negative offsets drew into the
fixed top-left chrome; seed text used a hard-coded width. Normalize node
positions around the graph bbox center, place the map in the lower
play area from canvas size, and reposition DOM seed input with resize.

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
d6790341cf
  1. 104
      src/stages/page_chase.ts

104
src/stages/page_chase.ts

@ -1,6 +1,7 @@
import { BaseScene } from "@/scene/BaseScene"; import { BaseScene } from "@/scene/BaseScene";
import { sceneManager } from "@/scene/SceneManager"; import { sceneManager } from "@/scene/SceneManager";
import { SceneType } from "@/enums/SceneType"; import { SceneType } from "@/enums/SceneType";
import { appRuntime } from "@/kernel/AppRuntime";
import { ChaseGameModel } from "@/game/chase/model"; import { ChaseGameModel } from "@/game/chase/model";
import type { Difficulty, NodeId } from "@/game/chase/types"; import type { Difficulty, NodeId } from "@/game/chase/types";
import { Container, Graphics, Text, TextStyle, type Ticker } from "pixi.js"; 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_RADIUS = 18;
const NODE_GAP = 58; const NODE_GAP = 58;
const END_ACTION_COOLDOWN_MS = 400; 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< type ChaseModelLike = Pick<
ChaseGameModel, ChaseGameModel,
@ -59,6 +65,9 @@ export default class ChaseScene extends BaseScene {
private unsubscribeStageChange?: () => void; private unsubscribeStageChange?: () => void;
private endActionLocked = false; private endActionLocked = false;
private lastEndActionAt = 0; private lastEndActionAt = 0;
private readonly onWindowResize = (): void => {
this.relayoutChrome();
};
constructor(options: ChaseSceneOptions = {}) { constructor(options: ChaseSceneOptions = {}) {
super("chase", SceneType.Normal); super("chase", SceneType.Normal);
@ -74,59 +83,54 @@ export default class ChaseScene extends BaseScene {
this.hudText = new Text({ this.hudText = new Text({
text: "Chase: loading...", text: "Chase: loading...",
style: new TextStyle({ style: new TextStyle({
fontSize: 20, fontSize: 16,
fill: 0xffffff, fill: 0xe2e8f0,
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif",
}), }),
}); });
this.hudText.position.set(24, 24);
this.stage.addChild(this.hudText); this.stage.addChild(this.hudText);
this.winCountText = new Text({ this.winCountText = new Text({
text: "", text: "",
style: new TextStyle({ style: new TextStyle({
fontSize: 18, fontSize: 17,
fill: 0xffffff, fill: 0xffffff,
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif",
}), }),
}); });
this.winCountText.position.set(24, 6);
this.stage.addChild(this.winCountText); this.stage.addChild(this.winCountText);
this.seedText = new Text({ this.seedText = new Text({
text: "", text: "",
style: new TextStyle({ style: new TextStyle({
fontSize: 18, fontSize: 17,
fill: 0xffffff, fill: 0xffffff,
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif",
}), }),
}); });
this.seedText.anchor.set(1, 0); this.seedText.anchor.set(1, 0);
this.seedText.position.set(760, 6);
this.stage.addChild(this.seedText); this.stage.addChild(this.seedText);
this.setupText = new Text({ this.setupText = new Text({
text: "", text: "",
style: new TextStyle({ style: new TextStyle({
fontSize: 16, fontSize: 14,
fill: 0xdbeafe, fill: 0x93c5fd,
fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif",
}), }),
}); });
this.setupText.position.set(24, 54);
this.stage.addChild(this.setupText); this.stage.addChild(this.setupText);
this.setupLayer.eventMode = "passive"; this.setupLayer.eventMode = "passive";
this.setupLayer.position.set(24, 84);
this.stage.addChild(this.setupLayer); this.stage.addChild(this.setupLayer);
this.buildSetupControls(); this.buildSetupControls();
this.graphLayer.eventMode = "passive"; this.graphLayer.eventMode = "passive";
this.graphLayer.position.set(140, 140);
this.stage.addChild(this.graphLayer); this.stage.addChild(this.graphLayer);
this.buildResultOverlay(); this.buildResultOverlay();
this.stage.addChild(this.resultOverlay); this.stage.addChild(this.resultOverlay);
this.relayoutChrome();
if (!this.model) { if (!this.model) {
this.model = this.model =
this.modelFactory?.() ?? this.modelFactory?.() ??
@ -139,6 +143,9 @@ export default class ChaseScene extends BaseScene {
} }
protected onSceneEnter(): void { protected onSceneEnter(): void {
if (typeof globalThis !== "undefined" && "addEventListener" in globalThis) {
globalThis.addEventListener("resize", this.onWindowResize);
}
if (!this.unsubscribeStageChange) { if (!this.unsubscribeStageChange) {
this.unsubscribeStageChange = sceneManager.onStageChange((current) => { this.unsubscribeStageChange = sceneManager.onStageChange((current) => {
if (current.name !== this.name) { if (current.name !== this.name) {
@ -152,6 +159,9 @@ export default class ChaseScene extends BaseScene {
} }
protected onSceneExit(): void { protected onSceneExit(): void {
if (typeof globalThis !== "undefined" && "removeEventListener" in globalThis) {
globalThis.removeEventListener("resize", this.onWindowResize);
}
this.unsubscribeStageChange?.(); this.unsubscribeStageChange?.();
this.unsubscribeStageChange = undefined; this.unsubscribeStageChange = undefined;
this.detachSeedInputBridge(); this.detachSeedInputBridge();
@ -179,6 +189,9 @@ export default class ChaseScene extends BaseScene {
child.destroy({ children: true }); 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 thiefNode = state.snapshot.graph.nodes[state.snapshot.thiefNodeId];
const availableMoves = thiefNode const availableMoves = thiefNode
? thiefNode.neighbors.filter((id) => id !== state.snapshot.guardNodeId) ? thiefNode.neighbors.filter((id) => id !== state.snapshot.guardNodeId)
@ -192,11 +205,10 @@ export default class ChaseScene extends BaseScene {
highlight.label = "move-highlight"; highlight.label = "move-highlight";
highlight.circle(0, 0, NODE_RADIUS + 8); highlight.circle(0, 0, NODE_RADIUS + 8);
highlight.stroke({ width: 3, color: 0xfacc15, alpha: 0.9 }); 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); this.graphLayer.addChild(highlight);
} }
const nodes = Object.values(state.snapshot.graph.nodes);
for (const node of nodes) { for (const node of nodes) {
const nodeView = new Graphics(); const nodeView = new Graphics();
nodeView.circle(0, 0, NODE_RADIUS); nodeView.circle(0, 0, NODE_RADIUS);
@ -210,7 +222,7 @@ export default class ChaseScene extends BaseScene {
: 0x64748b; : 0x64748b;
nodeView.fill({ color, alpha: 0.95 }); nodeView.fill({ color, alpha: 0.95 });
nodeView.stroke({ width: 2, color: 0xe2e8f0, alpha: 0.9 }); 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.eventMode = "static";
nodeView.cursor = "pointer"; nodeView.cursor = "pointer";
nodeView.on("pointerdown", () => this.handleNodeClick(node.id)); 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 { private handleNodeClick(targetNodeId: NodeId): void {
if (!this.model) { if (!this.model) {
return; return;
@ -363,7 +427,6 @@ export default class ChaseScene extends BaseScene {
private buildResultOverlay(): void { private buildResultOverlay(): void {
this.resultOverlay.visible = false; this.resultOverlay.visible = false;
this.resultOverlay.eventMode = "passive"; this.resultOverlay.eventMode = "passive";
this.resultOverlay.position.set(24, 170);
this.resultText = new Text({ this.resultText = new Text({
text: "", text: "",
@ -449,14 +512,17 @@ export default class ChaseScene extends BaseScene {
input.placeholder = "Seed (optional)"; input.placeholder = "Seed (optional)";
input.value = this.seedInput; input.value = this.seedInput;
input.style.position = "fixed"; input.style.position = "fixed";
input.style.left = "24px";
input.style.top = "130px";
input.style.zIndex = "10"; 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); input.addEventListener("input", this.handleSeedInputEvent);
doc.body.appendChild(input); doc.body.appendChild(input);
this.seedInputElement = input; this.seedInputElement = input;
this.inputAttached = true; this.inputAttached = true;
this.positionSeedInputDom();
} }
private detachSeedInputBridge(): void { private detachSeedInputBridge(): void {

Loading…
Cancel
Save