diff --git a/src/stages/games/chase/page_chase.ts b/src/stages/games/chase/page_chase.ts index 91bd63b..69e3c61 100644 --- a/src/stages/games/chase/page_chase.ts +++ b/src/stages/games/chase/page_chase.ts @@ -9,20 +9,44 @@ import { Container, Graphics, Text, TextStyle, type Ticker } from "pixi.js"; const NODE_RADIUS = 24; const NODE_GAP = 76; const END_ACTION_COOLDOWN_MS = 400; -const PAD_X = 20; -const PAD_Y = 12; +const PAD_X = 22; +const PAD_Y = 14; /** 顶栏 + 开局控件占用高度(用于把地图放到下方安全区) */ -const TOP_CHROME_H = 200; -const SEED_INPUT_WIDTH = 220; +const TOP_CHROME_H = 208; +/** 竖屏下顶栏最多占屏高的比例,避免棋盘被挤得太小 */ +const CHROME_HEIGHT_FRAC = 0.23; +const SEED_INPUT_MAX_W = 268; +const SEED_INPUT_H = 42; +/** 终局弹层卡片(与 relayout 居中计算一致) */ +const RESULT_CARD_W = 304; +const RESULT_CARD_H = 196; +const DIFF_BTN_W = 82; +const DIFF_BTN_H = 40; +const DIFF_BTN_GAP = 94; +const START_BTN_W = 108; +const START_BTN_H = 44; const TOKEN_LABEL_STYLE = new TextStyle({ - fontSize: 15, + fontSize: 16, fontWeight: "700", fill: 0xffffff, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", - stroke: { color: 0x0f172a, width: 3 }, + stroke: { color: 0x020617, width: 4 }, }); +function statusLabelZh(status: string): string { + switch (status) { + case "playing": + return "进行中"; + case "win": + return "胜利"; + case "lose": + return "终局"; + default: + return status; + } +} + type ChaseModelLike = Pick< ChaseGameModel, "getState" | "moveThief" | "newRound" | "retryRound" @@ -42,16 +66,21 @@ type ChaseSceneOptions = { export default class ChaseScene extends BaseScene { stage = new Container(); + private readonly bgLayer = new Graphics(); + private readonly chromePanel = new Graphics(); private model?: ChaseModelLike; private readonly modelFactory?: () => ChaseModelLike; private hudText?: Text; + private hudLegendText?: Text; private winCountText?: Text; private seedText?: Text; private setupText?: Text; private setupLayer = new Container(); private graphLayer = new Container(); + private readonly resultDimmer = new Graphics(); private resultOverlay = new Container(); private resultText = new Text({ text: "" }); + private resultSeedText = new Text({ text: "" }); private difficultyButtons: Record = { easy: new Graphics(), normal: new Graphics(), @@ -91,54 +120,97 @@ export default class ChaseScene extends BaseScene { this.stage.sortableChildren = true; this.stage.eventMode = "passive"; - this.hudText = new Text({ - text: "Chase: loading...", - style: new TextStyle({ - fontSize: 16, - fill: 0xe2e8f0, - fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", - }), - }); - this.stage.addChild(this.hudText); + this.bgLayer.eventMode = "none"; + this.chromePanel.eventMode = "none"; + this.stage.addChild(this.bgLayer); + this.stage.addChild(this.chromePanel); this.winCountText = new Text({ text: "", style: new TextStyle({ - fontSize: 17, - fill: 0xffffff, + fontSize: 22, + fontWeight: "700", + fill: 0xf8fafc, + letterSpacing: 0.5, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + dropShadow: { + alpha: 0.45, + angle: Math.PI / 2, + blur: 6, + color: 0x000000, + distance: 1, + }, }), }); + this.winCountText.zIndex = 10; this.stage.addChild(this.winCountText); this.seedText = new Text({ text: "", style: new TextStyle({ - fontSize: 17, - fill: 0xffffff, - fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + fontSize: 13, + fill: 0xc7d2fe, + letterSpacing: 0.3, + fontFamily: "ui-monospace, 'Cascadia Code', 'Microsoft YaHei', monospace", }), }); this.seedText.anchor.set(1, 0); + this.seedText.zIndex = 10; this.stage.addChild(this.seedText); + this.hudText = new Text({ + text: "Chase: loading...", + style: new TextStyle({ + fontSize: 15, + lineHeight: 20, + fontWeight: "500", + fill: 0xe2e8f0, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + this.hudText.zIndex = 10; + this.stage.addChild(this.hudText); + + this.hudLegendText = new Text({ + text: "", + style: new TextStyle({ + fontSize: 12, + lineHeight: 17, + fill: 0x64748b, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }), + }); + this.hudLegendText.zIndex = 10; + this.stage.addChild(this.hudLegendText); + this.setupText = new Text({ text: "", style: new TextStyle({ - fontSize: 14, - fill: 0x93c5fd, + fontSize: 12, + fill: 0x64748b, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", }), }); + this.setupText.zIndex = 10; this.stage.addChild(this.setupText); this.setupLayer.eventMode = "passive"; + this.setupLayer.zIndex = 11; this.stage.addChild(this.setupLayer); this.buildSetupControls(); this.graphLayer.eventMode = "passive"; + this.graphLayer.zIndex = 20; this.stage.addChild(this.graphLayer); + + this.resultDimmer.eventMode = "static"; + this.resultDimmer.cursor = "default"; + this.resultDimmer.visible = false; + this.resultDimmer.zIndex = 25; + this.stage.addChild(this.resultDimmer); + this.buildResultOverlay(); + this.resultOverlay.zIndex = 30; this.stage.addChild(this.resultOverlay); this.relayoutChrome(); @@ -188,16 +260,28 @@ export default class ChaseScene extends BaseScene { } const state = this.model.getState(); - this.winCountText.text = `成功次数: ${state.winCount}`; - this.seedText.text = `当前 seed:${state.snapshot.seed}`; - const roleHint = - state.snapshot.status === "playing" - ? "\n偷=你 · 兵=官兵 · 出=出口 · 点击黄圈可走格" + this.winCountText.text = `成功 · ${state.winCount}`; + this.seedText.text = `Seed ${state.snapshot.seed}`; + const statusZh = statusLabelZh(state.snapshot.status); + this.hudText.text = `第 ${state.turn} 回合 · ${statusZh} · 偷 ${state.snapshot.thiefNodeId} / 兵 ${state.snapshot.guardNodeId}`; + if (this.hudLegendText) { + const playing = state.snapshot.status === "playing"; + this.hudLegendText.visible = playing; + this.hudLegendText.text = playing + ? "偷=你 · 兵=官兵 · 出=出口 · 黄圈为可走格" : ""; - 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.setupText.text = `难度 ${this.selectedDifficulty} · ${this.difficultyLocked ? "对局中" : "开局前可调整"}`; this.drawDifficultyButtons(); this.renderResultOverlay(state.snapshot.status, state.loseReason); + this.updateSeedInputPlayVisibility(state.snapshot.status === "playing"); + if (this.resultSeedText) { + const ended = state.snapshot.status !== "playing"; + this.resultSeedText.visible = ended; + if (ended) { + this.resultSeedText.text = `本局种子 ${state.snapshot.seed}`; + } + } const oldChildren = this.graphLayer.removeChildren(); for (const child of oldChildren) { @@ -206,6 +290,20 @@ export default class ChaseScene extends BaseScene { const nodes = Object.values(state.snapshot.graph.nodes); const { offsetX, offsetY } = this.computeGraphCenterOffset(nodes); + const bounds = this.computeGraphPixelBounds(nodes, offsetX, offsetY); + + const boardMat = new Graphics(); + const matPad = 56; + boardMat.roundRect( + bounds.minPx - matPad, + bounds.minPy - matPad, + bounds.maxPx - bounds.minPx + matPad * 2, + bounds.maxPy - bounds.minPy + matPad * 2, + 36, + ); + boardMat.fill({ color: 0x030712, alpha: 0.78 }); + boardMat.stroke({ width: 1.5, color: 0x5b6578, alpha: 0.5 }); + this.graphLayer.addChild(boardMat); const thiefNode = state.snapshot.graph.nodes[state.snapshot.thiefNodeId]; const availableMoves = thiefNode @@ -228,17 +326,22 @@ export default class ChaseScene extends BaseScene { edgeLayer.moveTo(fromX, fromY); edgeLayer.lineTo(toX, toY); } - edgeLayer.stroke({ width: 3.5, color: 0xf1f5f9, alpha: 0.55 }); + edgeLayer.stroke({ width: 2, color: 0x818cf8, alpha: 0.32 }); this.graphLayer.addChild(edgeLayer); for (const nodeId of availableMoves) { const node = state.snapshot.graph.nodes[nodeId]; if (!node) { continue; } + const glow = new Graphics(); + glow.circle(0, 0, NODE_RADIUS + 18); + glow.fill({ color: 0xfbbf24, alpha: 0.12 }); + glow.position.set(node.q * NODE_GAP + offsetX, node.r * NODE_GAP + offsetY); + this.graphLayer.addChild(glow); const highlight = new Graphics(); highlight.label = "move-highlight"; - highlight.circle(0, 0, NODE_RADIUS + 12); - highlight.stroke({ width: 4, color: 0xfde047, alpha: 1 }); + highlight.circle(0, 0, NODE_RADIUS + 11); + highlight.stroke({ width: 3.5, color: 0xfbbf24, alpha: 1 }); highlight.position.set(node.q * NODE_GAP + offsetX, node.r * NODE_GAP + offsetY); this.graphLayer.addChild(highlight); } @@ -252,27 +355,51 @@ export default class ChaseScene extends BaseScene { 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); + if (isThief || isGuard) { + const halo = new Graphics(); + halo.circle(0, 0, NODE_RADIUS + 9); + halo.fill({ + color: isThief ? 0x22c55e : 0xf87171, + alpha: 0.22, + }); + cell.addChild(halo); + } + + const body = new Graphics(); + body.circle(0, 0, NODE_RADIUS); const color = isThief - ? 0x16a34a + ? 0x15803d : isGuard - ? 0xdc2626 + ? 0xb91c1c : isExit - ? 0x2563eb + ? 0x1d4ed8 : isMove - ? 0x0ea5e9 + ? 0x0e7490 : 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 }); + body.fill({ color, alpha: dim ? 0.38 : 1 }); + body.stroke({ + width: isThief || isGuard || isExit ? 3 : 2, + color: isExit ? 0x93c5fd : 0xf1f5f9, + alpha: dim ? 0.3 : 0.92, + }); cell.addChild(body); + const rim = new Graphics(); + rim.circle(0, 0, NODE_RADIUS + 2.5); + rim.stroke({ width: 1, color: 0x000000, alpha: dim ? 0.12 : 0.22 }); + cell.addChild(rim); + + const inner = new Graphics(); + inner.circle(-5, -5, NODE_RADIUS * 0.35); + inner.fill({ color: 0xffffff, alpha: isThief || isGuard || isExit ? 0.2 : 0.08 }); + cell.addChild(inner); + if (isThief) { const t = new Text({ text: "偷", style: TOKEN_LABEL_STYLE }); t.anchor.set(0.5, 0.5); @@ -298,19 +425,14 @@ export default class ChaseScene extends BaseScene { this.graphLayer.addChild(cell); } - this.applyGraphAutoscale(nodes, offsetX, offsetY); + this.applyGraphAutoscale(bounds, nodes.length); } - /** 在可视区内尽量放大棋盘(上限避免过大) */ - private applyGraphAutoscale( + private computeGraphPixelBounds( nodes: Array<{ q: number; r: number }>, offsetX: number, offsetY: number, - ): void { - if (nodes.length === 0) { - this.graphLayer.scale.set(1); - return; - } + ): { minPx: number; maxPx: number; minPy: number; maxPy: number; bw: number; bh: number } { let minPx = Infinity; let maxPx = -Infinity; let minPy = Infinity; @@ -325,9 +447,22 @@ export default class ChaseScene extends BaseScene { } const bw = Math.max(maxPx - minPx, 40); const bh = Math.max(maxPy - minPy, 40); + return { minPx, maxPx, minPy, maxPy, bw, bh }; + } + + /** 在可视区内尽量放大棋盘(上限避免过大) */ + private applyGraphAutoscale( + bounds: { bw: number; bh: number }, + nodeCount: number, + ): void { + if (nodeCount === 0) { + this.graphLayer.scale.set(1); + return; + } + const { bw, bh } = bounds; const { width, height } = appRuntime.game.getInfo(); - const playTop = Math.min(TOP_CHROME_H, height * 0.28); - const margin = 48; + const playTop = this.getPlayTop(height); + const margin = 44; const availW = Math.max(120, width - margin * 2); const availH = Math.max(120, height - playTop - margin * 2); const sx = availW / bw; @@ -360,32 +495,75 @@ export default class ChaseScene extends BaseScene { return { offsetX: -cx, offsetY: -cy }; } + private getPlayTop(screenHeight: number): number { + return Math.min(TOP_CHROME_H, screenHeight * CHROME_HEIGHT_FRAC); + } + 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 + 72); - this.setupLayer.position.set(PAD_X, safeTop + 98); + const playTop = this.getPlayTop(height); + + this.bgLayer.clear(); + this.bgLayer.rect(0, 0, width, height); + this.bgLayer.fill({ color: 0x040b14 }); + this.bgLayer.rect(0, 0, width, height * 0.52); + this.bgLayer.fill({ color: 0x132337, alpha: 0.55 }); + this.bgLayer.rect(0, height * 0.55, width, height * 0.45); + this.bgLayer.fill({ color: 0x020617, alpha: 0.65 }); + this.bgLayer.zIndex = 0; + + const panelPad = 14; + this.chromePanel.clear(); + this.chromePanel.roundRect(panelPad, 8, width - panelPad * 2, playTop - 12, 22); + this.chromePanel.fill({ color: 0x0b1220, alpha: 0.78 }); + this.chromePanel.stroke({ width: 1, color: 0x64748b, alpha: 0.35 }); + this.chromePanel.zIndex = 1; + + this.winCountText?.position.set(PAD_X + 8, safeTop + 6); + this.seedText?.position.set(width - PAD_X - 8, safeTop + 8); + this.hudText?.position.set(PAD_X + 8, safeTop + 34); + this.hudLegendText?.position.set(PAD_X + 8, safeTop + 56); + this.setupText?.position.set(PAD_X + 8, safeTop + 82); + this.setupLayer.position.set(PAD_X + 8, safeTop + 102); - 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.resultDimmer.clear(); + this.resultDimmer.rect(0, 0, width, height); + this.resultDimmer.fill({ color: 0x020617, alpha: 0.62 }); + this.resultOverlay.position.set( + (width - RESULT_CARD_W) / 2, + playCenterY - RESULT_CARD_H / 2, + ); - this.positionSeedInputDom(); + this.positionSeedInputDom(width); } - private positionSeedInputDom(): void { + /** 终局时隐藏 HTML 种子框,避免与结果弹层叠成「第二块面板」 */ + private updateSeedInputPlayVisibility(playing: boolean): void { if (!this.seedInputElement) { return; } - const topPx = this.setupLayer.position.y + 52; - this.seedInputElement.style.left = `${PAD_X}px`; + this.seedInputElement.style.display = playing ? "" : "none"; + } + + private positionSeedInputDom(viewWidth: number): void { + if (!this.seedInputElement) { + return; + } + const topPx = this.setupLayer.position.y + DIFF_BTN_H + 8; + const inputW = Math.min(SEED_INPUT_MAX_W, Math.max(200, viewWidth - (PAD_X + 8) * 2)); + this.seedInputElement.style.left = `${PAD_X + 8}px`; this.seedInputElement.style.top = `${topPx}px`; - this.seedInputElement.style.width = `${SEED_INPUT_WIDTH}px`; + this.seedInputElement.style.width = `${inputW}px`; + this.seedInputElement.style.height = `${SEED_INPUT_H}px`; + } + + /** 仅保留数字,避免非数字种子把布局撑乱;与开局随机逻辑一致 */ + private normalizeSeedInputRaw(raw: string): string { + return raw.replace(/\D/g, "").slice(0, 12); } private handleNodeClick(targetNodeId: NodeId): void { @@ -405,9 +583,9 @@ export default class ChaseScene extends BaseScene { } public setSeedInput(seedInput: string): void { - this.seedInput = seedInput; + this.seedInput = this.normalizeSeedInputRaw(seedInput); if (this.seedInputElement) { - this.seedInputElement.value = seedInput; + this.seedInputElement.value = this.seedInput; } this.refreshView(); } @@ -483,98 +661,147 @@ export default class ChaseScene extends BaseScene { private buildSetupControls(): void { this.setupLayer.removeChildren(); const difficulties: Difficulty[] = ["easy", "normal", "hard"]; + const labelStyle = new TextStyle({ + fontSize: 13, + fontWeight: "600", + fill: 0xf1f5f9, + letterSpacing: 0.4, + fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + }); difficulties.forEach((difficulty, index) => { const button = this.difficultyButtons[difficulty]; button.eventMode = "static"; button.cursor = "pointer"; button.removeAllListeners(); button.on("pointerdown", () => this.setDifficulty(difficulty)); - button.position.set(index * 96, 0); + button.position.set(index * DIFF_BTN_GAP, 0); this.setupLayer.addChild(button); const label = new Text({ text: difficulty, - style: new TextStyle({ - fontSize: 14, - fill: 0xffffff, - fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", - }), + style: labelStyle, }); label.anchor.set(0.5); - label.position.set(index * 96 + 38, 18); + label.position.set(index * DIFF_BTN_GAP + DIFF_BTN_W / 2, DIFF_BTN_H / 2); this.setupLayer.addChild(label); }); + const startX = 3 * DIFF_BTN_GAP + 12; this.startButton.eventMode = "static"; this.startButton.cursor = "pointer"; this.startButton.removeAllListeners(); this.startButton.on("pointerdown", () => this.startGame()); - this.startButton.position.set(304, 0); + this.startButton.position.set(startX, 0); this.setupLayer.addChild(this.startButton); const startLabel = new Text({ text: "开始游戏", style: new TextStyle({ - fontSize: 14, + fontSize: 15, + fontWeight: "700", fill: 0xffffff, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", }), }); startLabel.anchor.set(0.5); - startLabel.position.set(304 + 48, 18); + startLabel.position.set(startX + START_BTN_W / 2, START_BTN_H / 2); this.setupLayer.addChild(startLabel); this.drawDifficultyButtons(); this.startButton.clear(); - this.startButton.roundRect(0, 0, 96, 36, 8); - this.startButton.fill({ color: 0x2563eb, alpha: 0.95 }); + this.startButton.roundRect(0, 0, START_BTN_W, START_BTN_H, 14); + this.startButton.fill({ color: 0x1d4ed8, alpha: 1 }); + this.startButton.stroke({ width: 1.5, color: 0x93c5fd, alpha: 0.55 }); } private buildResultOverlay(): void { this.resultOverlay.visible = false; - this.resultOverlay.eventMode = "passive"; + this.resultOverlay.eventMode = "static"; + + const card = new Graphics(); + card.roundRect(0, 0, RESULT_CARD_W, RESULT_CARD_H, 24); + card.fill({ color: 0x0f172a, alpha: 0.98 }); + card.stroke({ width: 1.5, color: 0x64748b, alpha: 0.5 }); + this.resultOverlay.addChild(card); + const cardInner = new Graphics(); + cardInner.roundRect(6, 6, RESULT_CARD_W - 12, RESULT_CARD_H - 12, 18); + cardInner.stroke({ width: 1, color: 0xffffff, alpha: 0.06 }); + this.resultOverlay.addChild(cardInner); + + const cx = RESULT_CARD_W / 2; this.resultText = new Text({ text: "", style: new TextStyle({ fontSize: 24, - fill: 0xffffff, + fontWeight: "700", + fill: 0xf8fafc, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", + align: "center", + dropShadow: { + alpha: 0.35, + angle: Math.PI / 2, + blur: 4, + color: 0x000000, + distance: 1, + }, }), }); + this.resultText.anchor.set(0.5, 0); + this.resultText.position.set(cx, 22); this.resultOverlay.addChild(this.resultText); + this.resultSeedText = new Text({ + text: "", + style: new TextStyle({ + fontSize: 13, + fill: 0x94a3b8, + fontFamily: "ui-monospace, 'Cascadia Code', 'Microsoft YaHei', monospace", + align: "center", + wordWrap: true, + wordWrapWidth: RESULT_CARD_W - 40, + }), + }); + this.resultSeedText.anchor.set(0.5, 0); + this.resultSeedText.position.set(cx, 64); + this.resultOverlay.addChild(this.resultSeedText); + + const btnY = 124; 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.retryButton.position.set(36, btnY); + this.retryButton.roundRect(0, 0, 100, 42, 12); + this.retryButton.fill({ color: 0x1d4ed8, alpha: 1 }); + this.retryButton.stroke({ width: 1, color: 0x60a5fa, alpha: 0.45 }); this.resultOverlay.addChild(this.retryButton); - this.retryLabel = this.createOverlayLabel("重试", 43, 59); + this.retryLabel = this.createOverlayLabel("重试", 36 + 50, btnY + 21); this.resultOverlay.addChild(this.retryLabel); 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.newGameButton.position.set(168, btnY); + this.newGameButton.roundRect(0, 0, 100, 42, 12); + this.newGameButton.fill({ color: 0x0e7490, alpha: 1 }); + this.newGameButton.stroke({ width: 1, color: 0x5eead4, alpha: 0.4 }); this.resultOverlay.addChild(this.newGameButton); - this.newGameLabel = this.createOverlayLabel("新一局", 146, 59); + this.newGameLabel = this.createOverlayLabel("新一局", 168 + 50, btnY + 21); this.resultOverlay.addChild(this.newGameLabel); 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 }); + const restartX = (RESULT_CARD_W - 124) / 2; + this.restartButton.position.set(restartX, btnY); + this.restartButton.roundRect(0, 0, 124, 42, 12); + this.restartButton.fill({ color: 0x15803d, alpha: 1 }); + this.restartButton.stroke({ width: 1, color: 0x86efac, alpha: 0.45 }); this.resultOverlay.addChild(this.restartButton); - this.restartLabel = this.createOverlayLabel("重新开始", 54, 59); + this.restartLabel = this.createOverlayLabel("重新开始", restartX + 62, btnY + 21); this.resultOverlay.addChild(this.restartLabel); } @@ -582,7 +809,8 @@ export default class ChaseScene extends BaseScene { const label = new Text({ text, style: new TextStyle({ - fontSize: 14, + fontSize: 15, + fontWeight: "600", fill: 0xffffff, fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", }), @@ -596,11 +824,19 @@ export default class ChaseScene extends BaseScene { const drawButton = (difficulty: Difficulty): void => { const button = this.difficultyButtons[difficulty]; button.clear(); - button.roundRect(0, 0, 76, 36, 8); + button.roundRect(0, 0, DIFF_BTN_W, DIFF_BTN_H, 12); const selected = this.selectedDifficulty === difficulty; - const lockedColor = this.difficultyLocked ? 0x475569 : selected ? 0x0ea5e9 : 0x1f2937; - button.fill({ color: lockedColor, alpha: selected ? 0.95 : 0.88 }); - button.stroke({ width: selected ? 2 : 1, color: 0xe2e8f0, alpha: 0.9 }); + const base = this.difficultyLocked + ? 0x334155 + : selected + ? 0x1e3a8a + : 0x1e293b; + button.fill({ color: base, alpha: selected ? 1 : 0.9 }); + button.stroke({ + width: selected ? 2.5 : 1, + color: selected ? 0xfbbf24 : 0x475569, + alpha: selected ? 0.9 : 0.55, + }); button.cursor = this.difficultyLocked ? "not-allowed" : "pointer"; }; drawButton("easy"); @@ -615,20 +851,31 @@ export default class ChaseScene extends BaseScene { } const input = doc.createElement("input"); input.type = "text"; - input.placeholder = "Seed (optional)"; + input.inputMode = "numeric"; + input.pattern = "[0-9]*"; + input.autocomplete = "off"; + input.spellcheck = false; + input.placeholder = "数字种子,留空随机"; input.value = this.seedInput; input.style.position = "fixed"; input.style.zIndex = "10"; 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.style.padding = "0 14px"; + input.style.fontSize = "16px"; + input.style.lineHeight = `${SEED_INPUT_H}px`; + input.style.height = `${SEED_INPUT_H}px`; + input.style.maxHeight = `${SEED_INPUT_H}px`; + input.style.borderRadius = "10px"; + input.style.border = "1px solid rgba(100, 116, 139, 0.45)"; + input.style.background = "rgba(15, 23, 42, 0.92)"; + input.style.color = "#e2e8f0"; + input.style.outline = "none"; + input.style.boxShadow = "0 4px 16px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.05)"; input.addEventListener("input", this.handleSeedInputEvent); doc.body.appendChild(input); this.seedInputElement = input; this.inputAttached = true; - this.positionSeedInputDom(); + this.positionSeedInputDom(appRuntime.game.getInfo().width); } private detachSeedInputBridge(): void { @@ -648,6 +895,9 @@ export default class ChaseScene extends BaseScene { } if (this.stage.visible) { this.attachSeedInputBridge(); + if (this.model) { + this.updateSeedInputPlayVisibility(this.model.getState().snapshot.status === "playing"); + } } else { this.detachSeedInputBridge(); } @@ -655,7 +905,11 @@ export default class ChaseScene extends BaseScene { private readonly handleSeedInputEvent = (event: Event): void => { const target = event.target as HTMLInputElement | null; - this.seedInput = target?.value ?? ""; + const next = this.normalizeSeedInputRaw(target?.value ?? ""); + if (target && target.value !== next) { + target.value = next; + } + this.seedInput = next; this.refreshView(); }; @@ -675,11 +929,13 @@ export default class ChaseScene extends BaseScene { ): void { if (status === "playing") { this.resultOverlay.visible = false; + this.resultDimmer.visible = false; this.endActionLocked = false; return; } this.resultOverlay.visible = true; + this.resultDimmer.visible = true; if (status === "win") { this.resultText.text = "成功逃脱!"; this.restartButton.visible = true; diff --git a/src/stages/page_welcome.ts b/src/stages/page_welcome.ts deleted file mode 100644 index c1cd7a8..0000000 --- a/src/stages/page_welcome.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Button } from "@/components/Button"; -import { SceneType } from "@/enums/SceneType"; -import { appRuntime } from "@/kernel/AppRuntime"; -import { BaseScene } from "@/scene/BaseScene"; -import position from "@/utils/Position"; -import { Container, Graphics, Text, TextStyle } from "pixi.js"; - -type GuideItem = { - title: string; - hint: string; -}; - -const GUIDE_ITEMS: GuideItem[] = [ - { title: "1. 宝箱记忆", hint: "翻牌配对,考验短时记忆" }, - { title: "2. 节奏连击", hint: "跟随节拍点击,连击越高分越高" }, - { title: "3. 躲避训练", hint: "滑动角色闪避障碍,坚持更久" }, -]; - -export default class WelcomeScene extends BaseScene { - stage = new Container(); - private readonly game = appRuntime.game; - - private titleText?: Text; - private descText?: Text; - private detailText?: Text; - private backBtn?: Button; - private itemRows: Container[] = []; - - private readonly onResize = (): void => { - this.relayout(); - }; - - constructor() { - super("welcome", SceneType.Normal); - } - - protected async onSceneLayout(): Promise { - this.stage.sortableChildren = true; - this.stage.eventMode = "passive"; - - this.titleText = new Text({ - text: "子游戏引导", - style: new TextStyle({ - fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", - fontSize: 42, - fontWeight: "700", - fill: 0xffffff, - }), - }); - this.titleText.anchor.set(0.5); - this.stage.addChild(this.titleText); - - this.descText = new Text({ - text: "选择一项查看玩法说明", - style: new TextStyle({ - fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", - fontSize: 20, - fill: 0xdbeafe, - }), - }); - this.descText.anchor.set(0.5); - this.stage.addChild(this.descText); - - this.detailText = new Text({ - text: GUIDE_ITEMS[0].hint, - style: new TextStyle({ - fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", - fontSize: 18, - fill: 0xf8fafc, - }), - }); - this.detailText.anchor.set(0.5); - this.stage.addChild(this.detailText); - - this.itemRows = GUIDE_ITEMS.map((item, index) => this.createGuideRow(item, index)); - for (const row of this.itemRows) { - this.stage.addChild(row); - } - this.highlightRow(0); - - this.backBtn = new Button({ - text: "返回开始", - variant: "secondary", - fontSize: 24, - padding: { x: 64, y: 26 }, - position: () => position.get("center", "bottom", { y: -56, x: 0 }), - onClick: () => { - this.backBtn?.setDisabled(true); - this.backBtn?.setText("返回中..."); - void this.changeScene("init"); - }, - }); - this.stage.addChild(this.backBtn.getView()); - - this.relayout(); - } - - protected onSceneEnter(): void { - window.addEventListener("resize", this.onResize); - } - - protected onSceneExit(): void { - window.removeEventListener("resize", this.onResize); - } - - private createGuideRow(item: GuideItem, index: number): Container { - const row = new Container(); - row.eventMode = "static"; - row.cursor = "pointer"; - - const bg = new Graphics(); - bg.roundRect(0, 0, 560, 68, 14); - bg.fill({ color: 0x10223f, alpha: 0.92 }); - row.addChild(bg); - - const title = new Text({ - text: item.title, - style: new TextStyle({ - fontFamily: "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif", - fontSize: 24, - fill: 0xffffff, - }), - }); - title.anchor.set(0.5); - title.position.set(280, 34); - row.addChild(title); - - row.on("pointerdown", () => this.highlightRow(index)); - return row; - } - - private highlightRow(targetIndex: number): void { - this.itemRows.forEach((row, index) => { - const bg = row.children[0] as Graphics | undefined; - if (!bg) { - return; - } - bg.clear(); - if (index === targetIndex) { - bg.roundRect(0, 0, 560, 68, 14); - bg.fill({ color: 0x1d4ed8, alpha: 0.95 }); - } else { - bg.roundRect(0, 0, 560, 68, 14); - bg.fill({ color: 0x10223f, alpha: 0.92 }); - } - }); - - if (this.detailText) { - this.detailText.text = GUIDE_ITEMS[targetIndex].hint; - } - } - - private relayout(): void { - const { height: H } = this.game.getInfo(); - - this.titleText?.position.copyFrom(position.get("center", H * 0.12)); - this.descText?.position.copyFrom(position.get("center", H * 0.2)); - - this.itemRows.forEach((row, index) => { - row.position.copyFrom(position.get("center", H * 0.32 + index * 90)); - row.pivot.set(280, 34); - }); - - this.detailText?.position.copyFrom(position.get("center", H * 0.66)); - this.backBtn?.updateView(); - } -} diff --git a/tests/stages/page_chase.test.ts b/tests/stages/page_chase.test.ts index d6c3868..a6bcf08 100644 --- a/tests/stages/page_chase.test.ts +++ b/tests/stages/page_chase.test.ts @@ -118,15 +118,15 @@ describe("chase stage skeleton", () => { scene.refreshView(); const firstChildren = [...(scene as any).graphLayer.children]; - expect(firstChildren.length).toBe(3); + expect(firstChildren.length).toBe(4); scene.refreshView(); const secondChildren = [...(scene as any).graphLayer.children]; - expect(secondChildren.length).toBe(3); + expect(secondChildren.length).toBe(4); expect(secondChildren[0]).not.toBe(firstChildren[0]); - expect((firstChildren[0] as any).destroyed).toBe(true); - expect((firstChildren[1] as any).destroyed).toBe(true); - expect((firstChildren[2] as any).destroyed).toBe(true); + for (const c of firstChildren) { + expect((c as any).destroyed).toBe(true); + } }); it("init start entry routes to chase", () => { @@ -395,8 +395,8 @@ describe("chase stage skeleton", () => { } as any, }); void (scene as any).onSceneLayout(); - expect((scene as any).winCountText.text).toContain("成功次数: 0"); - expect((scene as any).seedText.text).toContain("当前 seed:123"); + expect((scene as any).winCountText.text).toContain("成功 · 0"); + expect((scene as any).seedText.text).toContain("Seed 123"); }); it("refreshes hud after state change", () => { @@ -423,8 +423,8 @@ describe("chase stage skeleton", () => { }); void (scene as any).onSceneLayout(); scene.refreshView(); - expect((scene as any).winCountText.text).toContain("成功次数: 2"); - expect((scene as any).seedText.text).toContain("当前 seed:999"); + expect((scene as any).winCountText.text).toContain("成功 · 2"); + expect((scene as any).seedText.text).toContain("Seed 999"); }); it("refreshes success counter on win state change", () => { @@ -452,7 +452,7 @@ describe("chase stage skeleton", () => { }); void (scene as any).onSceneLayout(); scene.refreshView(); - expect((scene as any).winCountText.text).toContain("成功次数: 1"); + expect((scene as any).winCountText.text).toContain("成功 · 1"); }); it("refreshes seed text after starting a new round", () => { @@ -476,7 +476,7 @@ describe("chase stage skeleton", () => { }); void (scene as any).onSceneLayout(); scene.refreshView(); - expect((scene as any).seedText.text).toContain("当前 seed:2027"); + expect((scene as any).seedText.text).toContain("Seed 2027"); }); it("renders move highlights matching available moves count", () => {