diff --git a/public/js/screens/game.js b/public/js/screens/game.js
index 47a1d602..3225f684 100644
--- a/public/js/screens/game.js
+++ b/public/js/screens/game.js
@@ -65,8 +65,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.js';
import { PHASE, GEO } from '../../../shared/constants.js';
-import { fxForm } from '../../../shared/protocol.js';
-import { html, Spinner, PhaseBanner, Icon, Button, MicroLabel, confirmDialog, closeAllDialogs, useTicker } from '../ui/components.js';
+import { html, Spinner, PhaseBanner, Icon, Button, confirmDialog, closeAllDialogs, useTicker } from '../ui/components.js';
import { useGameData, GIcon } from '../ui/gameComponents.js';
import { useFieldView } from '../ui/fieldHost.js';
import { TopBar, liveLp, ownLeaks, uniteRemaining, tempInfo, tempReadyReason } from '../ui/hud.js';
@@ -103,24 +102,17 @@ import { BriefingScreen } from './briefing.js';
import { BandDraftScreen } from './bandDraft.js';
import { ResultScreen } from './result.js';
import { net } from '../net.js';
-import { store, useStore, shallowEqual, serverNow, emptyMatch, isSpectating } from '../store.js';
+import { useStore, shallowEqual, serverNow, isSpectating } from '../store.js';
import { battleRunner } from '../battle/runner.js';
import { isClientCombat, observeTarget, teammateProgress, cameraLayers, layerCamera, sidesOf, resumedWatch } from '../battle/observe.js';
import { screenStrip, playerBonds, playerLayer, detailBondOwner, toggleBond, popupView } from '../ui/watchBonds.js';
-import { data, localAsset, getMode } from '../data.js';
+import { data, getMode } from '../data.js';
import { audio } from '../audio.js';
import { useDocClass, FullscreenButton } from '../ui/device.js';
-
-const cx = (...p) => p.flat().filter(Boolean).join(' ');
-const HUD_HZ_MS = 200;
-/** Range tiles of the selected unit (its own highlight group: the wheel's 'facing' group may be up at the same time). */
-const SEL_RANGE = Object.freeze({ group: 'selRange', color: 0xff9c33, fill: 0.3, line: 0.95 });
-/** The tile an armed merge-completing card's elite will take (its own group; gold like the promotion cue, render/fx.js). */
-const MERGE_HL = Object.freeze({ group: 'mergeTile', color: 0xffd45a, fill: 0.34, line: 1 });
-const STATE_EV = new Set(['spawn', 'die', 'deploy', 'status', 'skill']);
-/** Event tuples replayed when a field is entered late: the state-bearing kinds and the fx that change an enemy's model
- * form (shared/protocol.js fxForm — the field meta's UnitInfo `form` predates them). */
-const keepEarly = (e) => Array.isArray(e) && (STATE_EV.has(e[0]) || fxForm(e) !== undefined);
+// MatchEnded, PausedOverlay, the highlight styles and keepEarly live in ./game/*.js.
+import { HUD_HZ_MS, MERGE_HL, SEL_RANGE, cx } from './game/marks.js';
+import { keepEarly } from './game/early.js';
+import { MatchEnded, PausedOverlay } from './game/overlays.js';
/** Router for the in-match screens. */
export function GameScreen() {
@@ -147,36 +139,6 @@ export function GameScreen() {
${ended && !hasResult && mode !== 'result' ? html`<${MatchEnded} />` : null}`;
}
-/** The room went back to its lobby without a result (match aborted): offer the way back. */
-function MatchEnded() {
- return html`
-
- <${MicroLabel} tone="mint">SIMULATION CLOSED${MicroLabel}>
-
本局模拟已结束
-
同盟已返回等待室
- <${Button} variant="primary" size="lg" icon="chevronLeft" onClick=${() => store.set({ match: emptyMatch() })}>返回同盟/>
-
-
`;
-}
-
-/** Solo pause (m.public.paused): the field dims under the 暂停中 plate; 继续作战 resumes (g.pause off). */
-function PausedOverlay({ canResume, busy, onResume, onExit }) {
- const plate = localAsset('ui/battle', 'matte_pause');
- return html`
-
-
- PAUSED
-
暂停中
-
-
作战已暂停,计时与敌人行动均已停止
-
- ${onExit ? html`<${Button} variant="secondary" size="lg" icon="exit" onClick=${onExit}>放弃模拟/>` : null}
- ${canResume ? html`<${Button} variant="primary" size="lg" icon="play" loading=${busy} onClick=${onResume} data-autofocus>继续作战/>` : null}
-
-
-
`;
-}
-
// ---- match screen ----------------------------------------------------------------------------------------
function MatchScreen() {
@@ -1325,4 +1287,3 @@ function MatchScreen() {
<${ExitModal} open=${exitOpen} onClose=${() => setExitOpen(false)} solo=${solo} />
`;
}
-
diff --git a/public/js/screens/game/early.js b/public/js/screens/game/early.js
new file mode 100644
index 00000000..62f3c282
--- /dev/null
+++ b/public/js/screens/game/early.js
@@ -0,0 +1,9 @@
+// public/js/screens/game/early.js — which events are replayed when a field is entered late.
+
+import { fxForm } from '../../../../shared/protocol.js';
+
+export const STATE_EV = new Set(['spawn', 'die', 'deploy', 'status', 'skill']);
+
+/** Event tuples replayed when a field is entered late: the state-bearing kinds and the fx that change an enemy's model
+ * form (shared/protocol.js fxForm — the field meta's UnitInfo `form` predates them). */
+export const keepEarly = (e) => Array.isArray(e) && (STATE_EV.has(e[0]) || fxForm(e) !== undefined);
diff --git a/public/js/screens/game/marks.js b/public/js/screens/game/marks.js
new file mode 100644
index 00000000..1f11b3c8
--- /dev/null
+++ b/public/js/screens/game/marks.js
@@ -0,0 +1,11 @@
+// public/js/screens/game/marks.js — class-name join and the field highlight styles the match screen passes to the view.
+
+export const cx = (...p) => p.flat().filter(Boolean).join(' ');
+
+export const HUD_HZ_MS = 200;
+
+/** Range tiles of the selected unit (its own highlight group: the wheel's 'facing' group may be up at the same time). */
+export const SEL_RANGE = Object.freeze({ group: 'selRange', color: 0xff9c33, fill: 0.3, line: 0.95 });
+
+/** The tile an armed merge-completing card's elite will take (its own group; gold like the promotion cue, render/fx.js). */
+export const MERGE_HL = Object.freeze({ group: 'mergeTile', color: 0xffd45a, fill: 0.34, line: 1 });
diff --git a/public/js/screens/game/overlays.js b/public/js/screens/game/overlays.js
new file mode 100644
index 00000000..4ab312a4
--- /dev/null
+++ b/public/js/screens/game/overlays.js
@@ -0,0 +1,35 @@
+// public/js/screens/game/overlays.js — the match-ended plate and the solo pause plate.
+
+import { Button, MicroLabel, html } from '../../ui/components.js';
+import { emptyMatch, store } from '../../store.js';
+import { localAsset } from '../../data.js';
+
+/** The room went back to its lobby without a result (match aborted): offer the way back. */
+export function MatchEnded() {
+ return html`
+
+ <${MicroLabel} tone="mint">SIMULATION CLOSED${MicroLabel}>
+
本局模拟已结束
+
同盟已返回等待室
+ <${Button} variant="primary" size="lg" icon="chevronLeft" onClick=${() => store.set({ match: emptyMatch() })}>返回同盟/>
+
+
`;
+}
+
+/** Solo pause (m.public.paused): the field dims under the 暂停中 plate; 继续作战 resumes (g.pause off). */
+export function PausedOverlay({ canResume, busy, onResume, onExit }) {
+ const plate = localAsset('ui/battle', 'matte_pause');
+ return html`
+
+
+ PAUSED
+
暂停中
+
+
作战已暂停,计时与敌人行动均已停止
+
+ ${onExit ? html`<${Button} variant="secondary" size="lg" icon="exit" onClick=${onExit}>放弃模拟/>` : null}
+ ${canResume ? html`<${Button} variant="primary" size="lg" icon="play" loading=${busy} onClick=${onResume} data-autofocus>继续作战/>` : null}
+
+
+
`;
+}
diff --git a/test/match/feedback1-runner-forms.test.js b/test/match/feedback1-runner-forms.test.js
index 4c61b64b..4d6336c8 100644
--- a/test/match/feedback1-runner-forms.test.js
+++ b/test/match/feedback1-runner-forms.test.js
@@ -234,7 +234,8 @@ test('a hidden-tab backlog still beyond HELD_MAX after compaction: the first fra
test('screens/game.js buffers the form fx with the state-bearing events it replays when a field is entered late', () => {
const src = readFileSync(path.join(ROOT, 'public/js/screens/game.js'), 'utf8');
- assert.match(src, /const keepEarly = \(e\) => Array\.isArray\(e\) && \(STATE_EV\.has\(e\[0\]\) \|\| fxForm\(e\) !== undefined\);/);
+ const early = readFileSync(path.join(ROOT, 'public/js/screens/game/early.js'), 'utf8');
+ assert.match(early, /const keepEarly = \(e\) => Array\.isArray\(e\) && \(STATE_EV\.has\(e\[0\]\) \|\| fxForm\(e\) !== undefined\);/);
assert.match(src, /for \(const e of msg\.ev\) if \(keepEarly\(e\)\) buf\.push\(e\);/);
// a new m.field for the field on screen buffers its frames until the enter effect re-enters it (enterBattle resets)
assert.match(src, /if \(msg\.fieldId === lastFieldRef\.current\) reentryRef\.current = msg\.fieldId;/);
diff --git a/test/ui/playtest6-elite.test.js b/test/ui/playtest6-elite.test.js
index edf384c6..95afc3d6 100644
--- a/test/ui/playtest6-elite.test.js
+++ b/test/ui/playtest6-elite.test.js
@@ -171,6 +171,6 @@ describe('shop bar: the merge tag and the armed card', () => {
assert.match(src, /onArm=\$\{setArmedCard\}/, 'ShopBar reports the armed card');
assert.match(src, /mergeTarget\(priv, armedCard\.id, gd\.chess\)/, 'the target comes from gameLogic.mergeTarget');
assert.match(src, /view\.highlightTiles\(mergeAt \? \[\[mergeAt\.row, mergeAt\.col\]\] : \[\], MERGE_HL\)/, 'its own highlight group (cleared with [])');
- assert.match(src, /const MERGE_HL = Object\.freeze\(\{ group: 'mergeTile'/);
+ assert.match(read('public/js/screens/game/marks.js'), /const MERGE_HL = Object\.freeze\(\{ group: 'mergeTile'/);
});
});