diff --git a/src/stages/page_chase.ts b/src/stages/page_chase.ts index 36f20b4..63ed952 100644 --- a/src/stages/page_chase.ts +++ b/src/stages/page_chase.ts @@ -6,15 +6,23 @@ import { ChaseGameModel } from "@/game/chase/model"; import type { Difficulty, NodeId } from "@/game/chase/types"; import { Container, Graphics, Text, TextStyle, type Ticker } from "pixi.js"; -const NODE_RADIUS = 18; -const NODE_GAP = 58; +const NODE_RADIUS = 24; +const NODE_GAP = 76; const END_ACTION_COOLDOWN_MS = 400; const PAD_X = 20; const PAD_Y = 12; /** 顶栏 + 开局控件占用高度(用于把地图放到下方安全区) */ -const TOP_CHROME_H = 168; +const TOP_CHROME_H = 200; const SEED_INPUT_WIDTH = 220; +const TOKEN_LABEL_STYLE = new TextStyle({ + fontSize: 15, + fontWeight: "700", + fill: 0xffffff, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + stroke: { color: 0x0f172a, width: 3 }, +}); + type ChaseModelLike = Pick< ChaseGameModel, "getState" | "moveThief" | "newRound" | "retryRound" @@ -67,6 +75,9 @@ export default class ChaseScene extends BaseScene { private lastEndActionAt = 0; private readonly onWindowResize = (): void => { this.relayoutChrome(); + if (this.model && this.hudText) { + this.refreshView(); + } }; constructor(options: ChaseSceneOptions = {}) { @@ -179,7 +190,11 @@ export default class ChaseScene extends BaseScene { const state = this.model.getState(); this.winCountText.text = `成功次数: ${state.winCount}`; this.seedText.text = `当前 seed:${state.snapshot.seed}`; - this.hudText.text = `Turn ${state.turn} | ${state.snapshot.status} | thief=${state.snapshot.thiefNodeId} guard=${state.snapshot.guardNodeId}`; + const roleHint = + state.snapshot.status === "playing" + ? "\n偷=你 · 兵=官兵 · 出=出口 · 点击黄圈可走格" + : ""; + this.hudText.text = `回合 ${state.turn} | ${state.snapshot.status} | 偷@${state.snapshot.thiefNodeId} 兵@${state.snapshot.guardNodeId}${roleHint}`; this.setupText.text = `difficulty=${this.selectedDifficulty} | seed=${this.seedInput.trim() || "(auto)"} | locked=${this.difficultyLocked ? "yes" : "no"}`; this.drawDifficultyButtons(); this.renderResultOverlay(state.snapshot.status, state.loseReason); @@ -196,6 +211,25 @@ export default class ChaseScene extends BaseScene { const availableMoves = thiefNode ? thiefNode.neighbors.filter((id) => id !== state.snapshot.guardNodeId) : []; + const availableMoveSet = new Set(availableMoves); + + // 先画边,避免压住节点与高亮 + const edgeLayer = new Graphics(); + for (const [fromId, toId] of state.snapshot.graph.edgeList) { + const from = state.snapshot.graph.nodes[fromId]; + const to = state.snapshot.graph.nodes[toId]; + if (!from || !to) { + continue; + } + const fromX = from.q * NODE_GAP + offsetX; + const fromY = from.r * NODE_GAP + offsetY; + const toX = to.q * NODE_GAP + offsetX; + const toY = to.r * NODE_GAP + offsetY; + edgeLayer.moveTo(fromX, fromY); + edgeLayer.lineTo(toX, toY); + } + edgeLayer.stroke({ width: 3.5, color: 0xf1f5f9, alpha: 0.55 }); + this.graphLayer.addChild(edgeLayer); for (const nodeId of availableMoves) { const node = state.snapshot.graph.nodes[nodeId]; if (!node) { @@ -203,31 +237,103 @@ export default class ChaseScene extends BaseScene { } const highlight = new Graphics(); highlight.label = "move-highlight"; - highlight.circle(0, 0, NODE_RADIUS + 8); - highlight.stroke({ width: 3, color: 0xfacc15, alpha: 0.9 }); + highlight.circle(0, 0, NODE_RADIUS + 12); + highlight.stroke({ width: 4, color: 0xfde047, alpha: 1 }); highlight.position.set(node.q * NODE_GAP + offsetX, node.r * NODE_GAP + offsetY); this.graphLayer.addChild(highlight); } 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 + offsetX, node.r * NODE_GAP + offsetY); - nodeView.eventMode = "static"; - nodeView.cursor = "pointer"; - nodeView.on("pointerdown", () => this.handleNodeClick(node.id)); - this.graphLayer.addChild(nodeView); + const px = node.q * NODE_GAP + offsetX; + const py = node.r * NODE_GAP + offsetY; + const cell = new Container(); + cell.position.set(px, py); + cell.eventMode = "static"; + cell.cursor = "pointer"; + cell.on("pointerdown", () => this.handleNodeClick(node.id)); + + const body = new Graphics(); + body.circle(0, 0, NODE_RADIUS); + const isThief = node.id === state.snapshot.thiefNodeId; + const isGuard = node.id === state.snapshot.guardNodeId; + const isExit = node.id === state.snapshot.exitNodeId; + const isMove = availableMoveSet.has(node.id); + const color = isThief + ? 0x16a34a + : isGuard + ? 0xdc2626 + : isExit + ? 0x2563eb + : isMove + ? 0x0ea5e9 + : 0x475569; + const dim = + state.snapshot.status === "playing" && !isThief && !isGuard && !isExit && !isMove; + body.fill({ color, alpha: dim ? 0.42 : 0.98 }); + body.stroke({ width: isThief || isGuard ? 3 : 2, color: 0xf8fafc, alpha: dim ? 0.35 : 0.95 }); + cell.addChild(body); + + if (isThief) { + const t = new Text({ text: "偷", style: TOKEN_LABEL_STYLE }); + t.anchor.set(0.5, 0.5); + t.position.set(0, 0); + cell.addChild(t); + } else if (isGuard) { + const mark = new Graphics(); + mark.roundRect(-9, -11, 18, 20, 3); + mark.fill({ color: 0xe2e8f0, alpha: 0.95 }); + mark.stroke({ width: 1.5, color: 0x0f172a, alpha: 0.85 }); + cell.addChild(mark); + const t = new Text({ text: "兵", style: TOKEN_LABEL_STYLE }); + t.anchor.set(0.5, 0.5); + t.position.set(0, 0); + cell.addChild(t); + } else if (isExit) { + const t = new Text({ text: "出", style: TOKEN_LABEL_STYLE }); + t.anchor.set(0.5, 0.5); + t.position.set(0, 0); + cell.addChild(t); + } + + this.graphLayer.addChild(cell); } + + this.applyGraphAutoscale(nodes, offsetX, offsetY); + } + + /** 在可视区内尽量放大棋盘(上限避免过大) */ + private applyGraphAutoscale( + nodes: Array<{ q: number; r: number }>, + offsetX: number, + offsetY: number, + ): void { + if (nodes.length === 0) { + this.graphLayer.scale.set(1); + return; + } + let minPx = Infinity; + let maxPx = -Infinity; + let minPy = Infinity; + let maxPy = -Infinity; + for (const n of nodes) { + const px = n.q * NODE_GAP + offsetX; + const py = n.r * NODE_GAP + offsetY; + minPx = Math.min(minPx, px - NODE_RADIUS - 14); + maxPx = Math.max(maxPx, px + NODE_RADIUS + 14); + minPy = Math.min(minPy, py - NODE_RADIUS - 14); + maxPy = Math.max(maxPy, py + NODE_RADIUS + 14); + } + const bw = Math.max(maxPx - minPx, 40); + const bh = Math.max(maxPy - minPy, 40); + const { width, height } = appRuntime.game.getInfo(); + const playTop = Math.min(TOP_CHROME_H, height * 0.28); + const margin = 48; + const availW = Math.max(120, width - margin * 2); + const availH = Math.max(120, height - playTop - margin * 2); + const sx = availW / bw; + const sy = availH / bh; + const s = Math.min(1.85, Math.max(1.05, Math.min(sx, sy))); + this.graphLayer.scale.set(s); } /** 将节点包络中心移到局部原点,避免负坐标画进顶栏 */ @@ -260,8 +366,8 @@ export default class ChaseScene extends BaseScene { 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); + this.setupText?.position.set(PAD_X, safeTop + 72); + this.setupLayer.position.set(PAD_X, safeTop + 98); const playTop = Math.min(TOP_CHROME_H, height * 0.28); const playCenterY = playTop + (height - playTop) / 2; @@ -276,7 +382,7 @@ export default class ChaseScene extends BaseScene { if (!this.seedInputElement) { return; } - const topPx = this.setupLayer.position.y + 48; + const topPx = this.setupLayer.position.y + 52; this.seedInputElement.style.left = `${PAD_X}px`; this.seedInputElement.style.top = `${topPx}px`; this.seedInputElement.style.width = `${SEED_INPUT_WIDTH}px`;