Browse Source

feat(chase): enlarge board, clearer tokens, and stronger edges

Bigger node spacing, autoscale to fill play area, brighter edges,
role labels (偷/兵/出), dimmed non-adjacent cells, and HUD hints.

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
31fa6f8b2f
  1. 158
      src/stages/page_chase.ts

158
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`;

Loading…
Cancel
Save