feedback1 ui: folding the shop in prep moves the own board to the official shop-collapsed camera (GitHub #5)

While the own prep board is shown and the shop bar is folded, the game asks for the prep camera with shop: false
(left_prepare_camera_param / the boss prepare params) and the view keeps the bench above the folded shop's HUD band:
the higher of the folded tab and the corner buttons' hit areas (hudBands(kind, size, { shop: false }), cornerBand).
Unfolding returns to the shop camera. Never over the pen, a teammate's board or a battle; deferred while a piece is
dragged or its direction is chosen (gameLogic prepCameraFor / foldCamera). Below 768 px of width the corner stays on
one row while the bar is folded. Tile px at row 10: 1920x1080 109.8 -> 130.7, 844x390 35.9 -> 44.3,
756x366 31.4 -> 40.1, every bench pad fully pressable.
This commit is contained in:
sganggs committed 2026-10-03 16:14:43 +08:00
1 parent 842e1e13c4
commit 82c54897c7
8 files changed
+394 -33

No files matched your search

+5
View File
@@ -129,6 +129,11 @@ html:not(.sp-in-match) .toast-host { margin-top: var(--sa-t); }
.gm__corner { width: max(96px, calc(3 * max(.56rem, 34px) + .2rem)); } .gm__corner { width: max(96px, calc(3 * max(.56rem, 34px) + .2rem)); }
.dpanel:not(.dpanel--right) { bottom: calc(.44rem + max(1.51rem, 96px) * 55 / 151 + max(.56rem, 34px)); } .dpanel:not(.dpanel--right) { bottom: calc(.44rem + max(1.51rem, 96px) * 55 / 151 + max(.56rem, 34px)); }
} }
/* With the shop folded (收起) no bar covers the corner: one row again, so the folded shop's prep camera, which keeps the
bench above the corner's hit areas (ui/fieldHost.js hudBands), grows the board on narrow phones too (public issue #5). */
@media (max-width: 767px) {
.gm.is-collapsed .gm__corner { flex-wrap: nowrap; width: auto; }
}
/* Short landscape phones (≤ 460 CSS px tall, 1rem = 40 px): the lobby's two columns are compressed so the /* Short landscape phones (≤ 460 CSS px tall, 1rem = 40 px): the lobby's two columns are compressed so the
创建同盟 button and the join panel stay on screen without scrolling. The detail card's header is shorter (no 创建同盟 button and the join panel stay on screen without scrolling. The detail card's header is shorter (no
+15 -9
View File
@@ -3,11 +3,14 @@
// //
// const view = await createFieldView(host, { data, assets, audio, settings, padding, hud }) // const view = await createFieldView(host, { data, assets, audio, settings, padding, hud })
// view.setStage(stage) procedural tiles + devices (data/stages.json entry) // view.setStage(stage) procedural tiles + devices (data/stages.json entry)
// view.setCamera(kind, { rect, side, padding, instant }) 'prep'|'normal'|'unite'|'boss'('hidden'); animated // view.setCamera(kind, { rect, side, padding, instant, shop }) 'prep'|'normal'|'unite'|'boss'('hidden'); animated
// — the prep cameras (own board / Final Assault half) keep the bench // — the prep cameras (own board / Final Assault half) keep the bench
// and the field clear of `hud(kind, size)` = { top, bottom } px of // and the field clear of `hud(kind, size, { shop })` = { top, bottom } px
// DOM HUD along the top / bottom edge (projection.js clearHud; user // of DOM HUD along the top / bottom edge (projection.js clearHud; user
// playtest #5 item 9: the shop bar covered the bench on phones) // playtest #5 item 9: the shop bar covered the bench on phones); `shop:
// false` = the folded shop: the official shop-collapsed prep camera
// (left_prepare / *_boss_prepare) clear of the folded shop's band (public
// issue #5: folding the shop did not grow the board)
// view.setPrep(privateState, { editable, canPlace }) hand/temp/board pieces; editable enables drag & drop; a // view.setPrep(privateState, { editable, canPlace }) hand/temp/board pieces; editable enables drag & drop; a
// new board piece flashes (fx.deploy); a merge's elite — on the tile of // new board piece flashes (fx.deploy); a merge's elite — on the tile of
// the deployed copy it replaced, or on its bench slot — gets the // the deployed copy it replaced, or on its bench slot — gets the
@@ -374,7 +377,7 @@ export function releaseGl(renderer) {
* @param {HTMLElement} host * @param {HTMLElement} host
* @param {{ data?: any, assets?: any, audio?: any, settings?: { damageNumbers?: boolean, quality?: string }, * @param {{ data?: any, assets?: any, audio?: any, settings?: { damageNumbers?: boolean, quality?: string },
* padding?: object|((kind:string, size:{width:number,height:number}) => object), * padding?: object|((kind:string, size:{width:number,height:number}) => object),
* hud?: {top:number,bottom:number}|((kind:'prep'|'bossPrep', size:{width:number,height:number}) => {top:number,bottom:number}|null) }} [opts] * hud?: {top:number,bottom:number}|((kind:'prep'|'bossPrep', size:{width:number,height:number}, o:{shop:boolean}) => {top:number,bottom:number}|null) }} [opts]
*/ */
export async function createFieldView(host, options = {}) { export async function createFieldView(host, options = {}) {
if (!host || typeof host.appendChild !== 'function') throw new TypeError('createFieldView: host element required'); if (!host || typeof host.appendChild !== 'function') throw new TypeError('createFieldView: host element required');
@@ -650,10 +653,11 @@ export async function createFieldView(host, options = {}) {
} }
// the HUD bands the prep cameras keep the bench / field clear of (projection.js clearHud; user playtest #5 item 9): // the HUD bands the prep cameras keep the bench / field clear of (projection.js clearHud; user playtest #5 item 9):
// `opts.hud` = (kind, size) => { top, bottom } | null, or a fixed object; none → the plain official framing // `opts.hud` = (kind, size, { shop }) => { top, bottom } | null, or a fixed object; none → the plain official framing.
function hudBands(kind, sz) { // `shop: false` asks for the folded shop's band (the camera request's `shop: false`, public issue #5)
function hudBands(kind, sz, o) {
if (kind !== 'prep' && kind !== 'bossPrep') return null; if (kind !== 'prep' && kind !== 'bossPrep') return null;
if (typeof opts.hud === 'function') { try { return opts.hud(kind, sz) || null; } catch { return null; } } if (typeof opts.hud === 'function') { try { return opts.hud(kind, sz, o) || null; } catch { return null; } }
return opts.hud && typeof opts.hud === 'object' ? opts.hud : null; return opts.hud && typeof opts.hud === 'object' ? opts.hud : null;
} }
@@ -673,10 +677,12 @@ export async function createFieldView(host, options = {}) {
if (k === 'prep') rect = rect ? { ...rect, r0: Math.min(rect.r0, GEO.HAND_ROW) } : null; if (k === 'prep') rect = rect ? { ...rect, r0: Math.min(rect.r0, GEO.HAND_ROW) } : null;
// official configBlackBoard framing (render/projection.js presetCamera); the padding only matters for the // official configBlackBoard framing (render/projection.js presetCamera); the padding only matters for the
// fitted fallback (custom rects, portrait viewports); a prep camera keeps the bench and the field clear of the HUD // fitted fallback (custom rects, portrait viewports); a prep camera keeps the bench and the field clear of the HUD
// — `shop: false` (the folded shop, public issue #5) = the official shop-collapsed camera, clear of the folded
// shop's HUD band (re-evaluated on resize: camOpts keep the flag)
const vk = viewKind(kind, o); // (a 'prep' camera on the boss rows = the Final Assault prep) const vk = viewKind(kind, o); // (a 'prep' camera on the boss rows = the Final Assault prep)
return presetCamera(k, { width: sz.width, height: sz.height, padding: o.padding || defaultPadding(k, sz) }, { return presetCamera(k, { width: sz.width, height: sz.height, padding: o.padding || defaultPadding(k, sz) }, {
rect, side: o.side, half: !!o.half, shop: o.shop, fit: !!o.fit, config: stageRec?.config || null, rect, side: o.side, half: !!o.half, shop: o.shop, fit: !!o.fit, config: stageRec?.config || null,
hud: hudBands(vk, sz), hud: hudBands(vk, sz, { shop: o.shop !== false }),
}); });
} }
+27 -5
View File
@@ -29,7 +29,10 @@
// comes back as it was. Tapping an enemy in the pen (view pieceClick with `enemyKey` / a `preview` enemy unit) opens its // comes back as it was. Tapping an enemy in the pen (view pieceClick with `enemyKey` / a `preview` enemy unit) opens its
// detail card; the old enemy list lives on as the 敌方情报 tab of the 本局信息 dialog (left 🔍). // detail card; the old enemy list lives on as the 敌方情报 tab of the 本局信息 dialog (left 🔍).
// Every camera request of this screen goes through setCam (it remembers the camera the pen returns to); // Every camera request of this screen goes through setCam (it remembers the camera the pen returns to);
// `.gm[data-camera]` mirrors the current camera kind (E2E / styling). // `.gm[data-camera]` mirrors the current camera kind (E2E / styling). Folding the shop bar (收起) on the own prep board
// flies the camera to the official shop-collapsed framing and unfolding back to the shop camera (public issue #5:
// gameLogic prepCameraFor / foldCamera — never over the pen, a teammate's board or a battle; deferred while a piece is
// dragged or its direction is chosen).
// Equipment dropped on an operator whose two slots are used opens the equip-replace dialog (ui/equipReplace.js): the // Equipment dropped on an operator whose two slots are used opens the equip-replace dialog (ui/equipReplace.js): the
// player picks the equipped item to destroy → g.equip {itemUid, targetUid, replaceUid}; 取消 sends nothing. 销毁 is only // player picks the equipped item to destroy → g.equip {itemUid, targetUid, replaceUid}; 取消 sends nothing. 销毁 is only
// offered for loose items (equipped ones are locked, ui/facing.js itemDestroyable). // offered for loose items (equipped ones are locked, ui/facing.js itemDestroyable).
@@ -88,7 +91,8 @@ import {
phaseMode, phaseBanner, isCombatPhase, showDeadPill, isBossPhase, placementContext, canPlace, boardTargets, dropIntent, phaseMode, phaseBanner, isCombatPhase, showDeadPill, isBossPhase, placementContext, canPlace, boardTargets, dropIntent,
snapHud, activeBubbles, shortcutFor, shortcutBlocked, closesOnFieldPress, phaseTotalSeconds, homeFieldId, ownFieldId, normalizeSp, sortedPlayers, snapHud, activeBubbles, shortcutFor, shortcutBlocked, closesOnFieldPress, phaseTotalSeconds, homeFieldId, ownFieldId, normalizeSp, sortedPlayers,
countdownState, shopBlockReason, stageOverrides, effectiveStage, watchTarget, dropFailureReason, countdownState, shopBlockReason, stageOverrides, effectiveStage, watchTarget, dropFailureReason,
previewEnemyKey, prepCamera, deployFieldOf, panelSide, panelSlots, bondPopupPlace, chessLoadout, unitLoadout, mergeTarget, modeOffBonds, previewEnemyKey, prepCamera, prepCameraFor, foldCamera, deployFieldOf, panelSide, panelSlots, bondPopupPlace, chessLoadout, unitLoadout,
mergeTarget, modeOffBonds,
} from '../ui/gameLogic.js'; } from '../ui/gameLogic.js';
import { toast } from '../ui/toasts.js'; import { toast } from '../ui/toasts.js';
import { BriefingScreen } from './briefing.js'; import { BriefingScreen } from './briefing.js';
@@ -225,6 +229,7 @@ function MatchScreen() {
const home = homeFieldId(pub, myId); const home = homeFieldId(pub, myId);
const watchingOther = !!watching && watching !== home && watching !== ownFieldId(myId); const watchingOther = !!watching && watching !== home && watching !== ownFieldId(myId);
const editable = phase === PHASE.PREP && !!priv && alive && !priv.ready && !watchingOther; const editable = phase === PHASE.PREP && !!priv && alive && !priv.ready && !watchingOther;
const showShop = !!priv && alive && (phase === PHASE.PREP || phase === PHASE.SP_DRAFT || phase === PHASE.ROUND_START) && !watchingOther;
const layersDisabled = phase === PHASE.UNITE || isBossPhase(phase); const layersDisabled = phase === PHASE.UNITE || isBossPhase(phase);
const sp = phase === PHASE.SP_DRAFT ? normalizeSp(pub?.sp, players) : null; const sp = phase === PHASE.SP_DRAFT ? normalizeSp(pub?.sp, players) : null;
const total = phaseTotalSeconds(pub, gd.config, myId); const total = phaseTotalSeconds(pub, gd.config, myId);
@@ -274,6 +279,9 @@ function MatchScreen() {
const prepCamSeen = useRef(prepCamKey); // the prep camera last requested const prepCamSeen = useRef(prepCamKey); // the prep camera last requested
const camRef = useRef({ kind: 'prep', opts: { rect: { ...GEO.NORMAL_RECT }, side: 'L' } }); const camRef = useRef({ kind: 'prep', opts: { rect: { ...GEO.NORMAL_RECT }, side: 'L' } });
const penRef = useRef({ on: false, collapsed: false }); const penRef = useRef({ on: false, collapsed: false });
// the shop bar is shown folded (收起; the pen folds it for itself: the player's own state is the one it returns to) —
// the own prep board then takes the official shop-collapsed camera (public issue #5, gameLogic prepCameraFor)
const shopFolded = showShop && (pen ? penRef.current.collapsed : collapsed);
const cancelFacingRef = useRef(() => {}); const cancelFacingRef = useRef(() => {});
const setCam = useCallback((kind, opts) => { const setCam = useCallback((kind, opts) => {
camRef.current = { kind, opts: opts || {} }; camRef.current = { kind, opts: opts || {} };
@@ -360,7 +368,8 @@ function MatchScreen() {
// the battle we just left (or whatever was stored before mount) must not be re-entered next combat; // the battle we just left (or whatever was stored before mount) must not be re-entered next combat;
// an m.field that arrives during prep (the upcoming battle) is a new object and will be entered // an m.field that arrives during prep (the upcoming battle) is a new object and will be entered
staleFieldRef.current = field; staleFieldRef.current = field;
setCam(prepCam.kind, prepCam.opts); const pc = prepCameraFor(pub, myId, shopFolded);
setCam(pc.kind, pc.opts);
prepCamSeen.current = prepCamKey; prepCamSeen.current = prepCamKey;
viewModeRef.current = 'prep'; viewModeRef.current = 'prep';
lastFieldRef.current = null; lastFieldRef.current = null;
@@ -483,9 +492,23 @@ function MatchScreen() {
if (!view || viewModeRef.current !== 'prep' || !showPrep) return; if (!view || viewModeRef.current !== 'prep' || !showPrep) return;
if (live.current.facing) cancelFacingRef.current(); if (live.current.facing) cancelFacingRef.current();
setSel(null); setSel(null);
setCam(prepCam.kind, prepCam.opts); const pc = prepCameraFor(pub, myId, shopFolded);
setCam(pc.kind, pc.opts);
}, [view, prepCamKey, showPrep]); }, [view, prepCamKey, showPrep]);
// the shop bar folded (收起) or unfolded on the own prep board (public issue #5: the board did not grow, while the
// battle and scouting views had no bar to make room for): the official shop-collapsed camera or the shop camera
// again, flown like any camera change. Never over the pen or a teammate's board or in battle; deferred while a piece
// is dragged or its direction is chosen (the drop target and the wheel sit on tiles of the camera in use) — this
// effect runs again when that ends (gameLogic foldCamera). Picking reads the camera of every frame (render/pick.js).
useEffect(() => {
const next = foldCamera({
pub, myId, folded: shopFolded, ownPrep: !!view && viewModeRef.current === 'prep' && showPrep,
pen, busy: !!drag || !!facing, current: camRef.current,
});
if (next) setCam(next.kind, next.opts);
}, [view, shopFolded, showPrep, pen, !!drag, !!facing, prepCamKey]);
// 联防 / 最终攻势: the ‹ › pill moves the camera between the field's halves and 全景 (research 09 §3.1) // 联防 / 最终攻势: the ‹ › pill moves the camera between the field's halves and 全景 (research 09 §3.1)
useEffect(() => { useEffect(() => {
if (!view || !field || !field.local || !isCombatPhase(phase)) return; if (!view || !field || !field.local || !isCombatPhase(phase)) return;
@@ -1050,7 +1073,6 @@ function MatchScreen() {
// ---- render --------------------------------------------------------------------------------------------------- // ---- render ---------------------------------------------------------------------------------------------------
const readyCount = players.filter((p) => p.ready || p.status === 'ready').length; const readyCount = players.filter((p) => p.ready || p.status === 'ready').length;
const aliveCount = players.filter((p) => p.alive !== false && p.status !== 'left').length; const aliveCount = players.filter((p) => p.alive !== false && p.status !== 'left').length;
const showShop = !!priv && alive && (phase === PHASE.PREP || phase === PHASE.SP_DRAFT || phase === PHASE.ROUND_START) && !watchingOther;
// the own battle is over: the server says so (status done) or — client-side combat — the local simulation just ended // the own battle is over: the server says so (status done) or — client-side combat — the local simulation just ended
const localDone = cc && !!battleState && battleState.own && !battleState.watch && battleState.done && battleState.fieldId === ownFieldId(myId); const localDone = cc && !!battleState && battleState.own && !battleState.watch && battleState.done && battleState.fieldId === ownFieldId(myId);
// (client-side combat: only in 各自行动 — 联防 observers just watch the 联防 field, research 09 §3.1) // (client-side combat: only in 各自行动 — 联防 observers just watch the 联防 field, research 09 §3.1)
+61 -17
View File
@@ -43,41 +43,85 @@ export function hudPadding(kind, size) {
* 2.15rem measured) and the shop bar's top edge above the viewport's bottom (css/screens/game-shop.css .shopbar * 2.15rem measured) and the shop bar's top edge above the viewport's bottom (css/screens/game-shop.css .shopbar
* bottom .2rem + .shopbar__row padding .1rem ×2 + card height 2.24rem, plus its 2 px + 1 px borders). The shop bar * bottom .2rem + .shopbar__row padding .1rem ×2 + card height 2.24rem, plus its 2 px + 1 px borders). The shop bar
* sits on the viewport's bottom edge even on a notched phone (css/devices.css, DESIGN §18.1). * sits on the viewport's bottom edge even on a notched phone (css/devices.css, DESIGN §18.1).
* The folded shop (public issue #5): the tab's top edge (.shopbar-tab bottom .2rem + padding .08rem ×2 + its .44rem
* button, plus its 2 px + 1 px borders; it sits inside the HUD layer, above the bottom safe-area inset) and the corner
* buttons' top edge (css/screens/game.css .gm__corner bottom .24rem + a .56rem row — the fallback when the corner
* cannot be measured: on phones its buttons grow to 34 px and below 768 CSS px of width they wrap into two rows,
* css/devices.css).
*/ */
export const HUD_REM = Object.freeze({ bondStripBottom: 2.16, shopBarTop: 2.64, shopBarBorderPx: 3 }); export const HUD_REM = Object.freeze({
bondStripBottom: 2.16, shopBarTop: 2.64, shopBarBorderPx: 3, shopTabTop: 0.8, shopTabBorderPx: 3, cornerTop: 0.8,
});
/** /**
* CSS px of HUD along the top edge (top bar + bond strip) and the bottom edge (the shop bar) of the viewport during * CSS px of HUD along the top edge (top bar + bond strip) and the bottom edge of the viewport during prep — the own
* prep — the own board ('prep') or the Final Assault half ('bossPrep'); null for every other camera. The prep camera * board ('prep') or the Final Assault half ('bossPrep'); null for every other camera. The prep camera keeps the bench /
* keeps the bench / temp rows and the field's back row clear of them (render/projection.js clearHud; user playtest * temp rows and the field's back row clear of them (render/projection.js clearHud; user playtest #5 item 9: the rem
* #5 item 9: the rem floor of 40 px makes the HUD relatively taller on phones in landscape and the shop bar covered * floor of 40 px makes the HUD relatively taller on phones in landscape and the shop bar covered the bench).
* the bench). The prep camera is the shop camera whether or not the bar is collapsed, so the band assumes the bar — * The bottom band is the shop bar's — also for an eliminated player's own board (no shop bar: the band only costs size
* also for an eliminated player's own board (no shop bar: the band only costs size there, while a camera following * there, while a camera following the bar's presence would have to re-frame whenever it appears, e.g. when the private
* the bar's presence would have to re-frame whenever it appears, e.g. when the private state arrives after the prep * state arrives after the prep camera was set) — unless `opts.shop === false`: the player folded the shop (收起) and the
* camera was set). Scouting a teammate's board uses the 'normal' camera: no band. An armed shop card (two-tap buy, * own prep board moved to the official shop-collapsed camera (public issue #5: the board did not grow; screens/game.js
* css/screens/game-shop.css .scard.is-armed) rises 4 px above the bar's top on a phone and covers the bench pads' * asks for it only while a folded bar is shown). Its band is the higher of the folded tab (bottom right) and the corner
* near corners by ≈ 3 px while it stays armed — less than under the unchanged official camera at 1920×1080 (13 px * buttons' hit areas (交流 / ⚙ / 📖 / ⛶, bottom left, measured by `cornerBand`: on a narrow phone two rows of 34 px buttons
* above the bar, ≈ 11 px over the pads). * with 44 px touch areas) — under the official collapsed camera the bench's left pads reach under the corner, and with
* the tab alone it would cover bench pad 0 at the user's 756×366 Android and the near edge of pads 0–2 at 844×390; both
* are 1-D bands like the bar's, so every bench pad stays fully pressable. Scouting a
* teammate's board uses the 'normal' camera: no band. An armed shop card (two-tap buy, css/screens/game-shop.css
* .scard.is-armed) rises 4 px above the bar's top on a phone and covers the bench pads' near corners by ≈ 3 px while it
* stays armed — less than under the unchanged official camera at 1920×1080 (13 px above the bar, ≈ 11 px over the pads).
* @param {string} kind * @param {string} kind
* @param {{ width: number, height: number }} size * @param {{ width: number, height: number }} size
* @param {{ shop?: boolean }} [opts] `shop: false` = the folded shop's band
* @returns {{ top: number, bottom: number }|null} * @returns {{ top: number, bottom: number }|null}
*/ */
export function hudBands(kind, size) { export function hudBands(kind, size, opts) {
if (kind !== 'prep' && kind !== 'bossPrep') return null; if (kind !== 'prep' && kind !== 'bossPrep') return null;
const folded = !!opts && opts.shop === false;
const h = size?.height || 1080;
let rem = 100; let rem = 100;
let safeTop = 0; let safeTop = 0;
let safeBottom = 0;
let corner = 0;
try { try {
rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 100; rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 100;
// the HUD layer starts below the top safe-area inset (css/devices.css .gm__hud) // the HUD layer starts below the top safe-area inset and ends above the bottom one (css/devices.css .gm__hud)
safeTop = Math.max(0, document.querySelector('.gm__hud')?.getBoundingClientRect().top || 0); const hud = document.querySelector('.gm__hud')?.getBoundingClientRect();
safeTop = Math.max(0, hud?.top || 0);
if (folded) {
if (hud && hud.bottom > 0) safeBottom = Math.max(0, h - hud.bottom);
corner = cornerBand(h);
}
} catch { /* ignore */ } } catch { /* ignore */ }
const h = size?.height || 1080; const bottom = folded
? Math.max(safeBottom + rem * HUD_REM.shopTabTop + HUD_REM.shopTabBorderPx, corner || safeBottom + rem * HUD_REM.cornerTop)
: rem * HUD_REM.shopBarTop + HUD_REM.shopBarBorderPx;
return { return {
top: Math.min(h * 0.4, safeTop + rem * HUD_REM.bondStripBottom), top: Math.min(h * 0.4, safeTop + rem * HUD_REM.bondStripBottom),
bottom: Math.min(h * 0.4, rem * HUD_REM.shopBarTop + HUD_REM.shopBarBorderPx), bottom: Math.min(h * 0.4, bottom),
}; };
} }
/**
* CSS px from the viewport's bottom edge (`h` high) up to the top of the corner buttons' hit areas (交流 / ⚙ / 📖 / ⛶,
* .gm__corner), 0 when they are not on the page. On a touch screen every one of them takes taps from an invisible area
* of at least --tap-min (44 px) centred on it (css/devices.css touch targets), 5 px above a 34 px phone button — a bench
* pad edge there is not pressable.
* @param {number} h
*/
function cornerBand(h) {
const btns = document.querySelectorAll('.gm__corner .gm__gear, .gm__corner .ewheel__btn');
if (!btns || !btns.length) return 0;
const root = document.documentElement;
const tap = root?.classList?.contains('sp-coarse') ? parseFloat(getComputedStyle(root).getPropertyValue('--tap-min')) || 0 : 0;
let top = Infinity;
for (const b of btns) {
const r = b.getBoundingClientRect();
if (r && r.height > 0) top = Math.min(top, r.top + r.height / 2 - Math.max(r.height, tap) / 2);
}
return top < Infinity ? Math.max(0, h - top) : 0;
}
function renderPref() { function renderPref() {
try { try {
const q = new URLSearchParams(globalThis.location?.search || '').get('render'); const q = new URLSearchParams(globalThis.location?.search || '').get('render');
+31
View File
@@ -91,6 +91,37 @@ export function prepCamera(pub, myId) {
return { kind: 'bossPrep', opts: { side: idx % 2 === 1 ? 'R' : 'L' } }; return { kind: 'bossPrep', opts: { side: idx % 2 === 1 ? 'R' : 'L' } };
} }
/**
* The own prep board's camera request with the shop bar's state (public issue #5 "准备阶段,收起商店界面时,界面并不会进行
* 缩放"): `prepCamera` plus `shop: !folded` — a folded shop (收起) takes the official shop-collapsed camera
* (configBlackBoard left_prepare_camera_param / *_boss_prepare_*, render/projection.js presetCamera `shop: false`) and
* the folded shop's HUD band (ui/fieldHost.js hudBands), an open one the shop camera. `folded` = the shop bar is shown
* and folded (an eliminated player's board, which shows no bar, keeps the shop camera).
* @returns {{ kind: 'prep'|'bossPrep', opts: { rect?: object, side: 'L'|'R', shop: boolean } }}
*/
export function prepCameraFor(pub, myId, folded = false) {
const c = prepCamera(pub, myId);
return { kind: c.kind, opts: { ...c.opts, shop: !folded } };
}
/**
* The camera request a fold / unfold of the shop bar needs right now (public issue #5), or null: only while the own prep
* board is on screen (`ownPrep`: prep, not a teammate's scouted board, not a battle), never over the enemy pen (`pen`: it
* folds the shop itself and returns to the camera it left), deferred while a piece is dragged or its direction is chosen
* (`busy`: the drop target and the wheel sit on tiles of the camera in use — the caller asks again once that ends), and
* nothing when the current request (`current` { kind, opts }) already has that shop state.
* @param {{ pub: any, myId: string|null, ownPrep: boolean, folded: boolean, pen?: boolean, busy?: boolean,
* current?: { kind: string, opts?: { shop?: boolean } }|null }} s
* @returns {{ kind: 'prep'|'bossPrep', opts: object }|null}
*/
export function foldCamera(s) {
if (!s || !s.ownPrep || s.pen || s.busy) return null;
const want = prepCameraFor(s.pub, s.myId, !!s.folded);
const cur = s.current;
if (cur && cur.kind === want.kind && (cur.opts?.shop !== false) === want.opts.shop) return null;
return want;
}
/** /**
* The field the own pieces are deployed on (server/match/Match.js deployFieldOf): 'bossL' / 'bossR' in the prep of a * The field the own pieces are deployed on (server/match/Match.js deployFieldOf): 'bossL' / 'bossR' in the prep of a
* boss round (the same pairing as `prepCamera`), else 'normal'. Placement legality (`placementContext` → `deployMap` * boss round (the same pairing as `prepCamera`), else 'normal'. Placement legality (`placementContext` → `deployMap`
+212
View File
@@ -0,0 +1,212 @@
// Public issue #5 (v0.1.1): "准备阶段,收起商店界面时,界面并不会进行缩放(观战及作战时无此现象)" — folding the shop (收起) in prep
// left the own board at the shop camera. Now the own prep board takes the official shop-collapsed camera
// (configBlackBoard left_prepare_camera_param / *_boss_prepare_*) while the shop bar is shown folded, keeping the bench
// above the folded shop's HUD band (the tab and the corner buttons' hit areas), and returns to the shop camera when it is
// unfolded; never over the pen, a teammate's board or a battle; deferred during a drag or the direction step.
// gameLogic prepCameraFor / foldCamera (the choice), ui/fieldHost.js hudBands(kind, size, { shop: false }) (the band),
// render/app.js (the band follows the request's `shop`), screens/game.js (the effect), css/devices.css (one-row corner).
// Browser measurement: the headless-Chrome checks of DESIGN §21 (fold / unfold at 1920×1080, 844×390, 756×366).
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { GEO } from '../../shared/constants.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
const { prepCameraFor, foldCamera, prepCamera } = await import('../../public/js/ui/gameLogic.js');
const { hudBands, HUD_REM } = await import('../../public/js/ui/fieldHost.js');
const { presetCamera, tileQuad } = await import('../../public/js/render/projection.js');
const pl = (id, seat, alive = true) => ({ playerId: id, seat, alive });
const pubAt = (round, players = [pl('a', 0), pl('b', 1)]) => ({ round, bossRound: 14, hiddenRound: 15, players });
describe('the camera choice (gameLogic prepCameraFor / foldCamera)', () => {
test('prepCameraFor: the own prep camera with the shop state — open = shop camera, folded = shop-collapsed', () => {
assert.deepEqual(prepCameraFor(pubAt(6), 'a'), { kind: 'prep', opts: { rect: { ...GEO.NORMAL_RECT }, side: 'L', shop: true } });
assert.deepEqual(prepCameraFor(pubAt(6), 'a', true), { kind: 'prep', opts: { rect: { ...GEO.NORMAL_RECT }, side: 'L', shop: false } });
// the Final Assault prep: the player's half of the boss field, with the same flag
assert.deepEqual(prepCameraFor(pubAt(14), 'b', true), { kind: 'bossPrep', opts: { side: 'R', shop: false } });
assert.deepEqual(prepCameraFor(pubAt(14), 'a'), { kind: 'bossPrep', opts: { side: 'L', shop: true } });
// everything but the flag is prepCamera's
for (const [pub, me] of [[pubAt(3), 'a'], [pubAt(15), 'b'], [null, 'a']]) {
const { shop, ...rest } = prepCameraFor(pub, me, true).opts;
assert.equal(shop, false);
assert.deepEqual(rest, prepCamera(pub, me).opts);
}
});
const base = { pub: pubAt(6), myId: 'a', ownPrep: true, pen: false, busy: false };
const open = { kind: 'prep', opts: { rect: { ...GEO.NORMAL_RECT }, side: 'L', shop: true } };
const folded = { kind: 'prep', opts: { rect: { ...GEO.NORMAL_RECT }, side: 'L', shop: false } };
test('foldCamera: a fold asks for the shop-collapsed camera, an unfold for the shop camera again, else nothing', () => {
assert.deepEqual(foldCamera({ ...base, folded: true, current: open }), folded);
assert.deepEqual(foldCamera({ ...base, folded: false, current: folded }), open);
assert.equal(foldCamera({ ...base, folded: true, current: folded }), null, 'already folded');
assert.equal(foldCamera({ ...base, folded: false, current: open }), null, 'already open');
// a request made before this rule (no shop flag) counts as the shop camera
assert.equal(foldCamera({ ...base, folded: false, current: { kind: 'prep', opts: { side: 'L' } } }), null);
assert.deepEqual(foldCamera({ ...base, folded: true, current: { kind: 'prep', opts: { side: 'L' } } }), folded);
});
test('foldCamera: own prep board only — not a teammate\'s board, a battle, the pen', () => {
assert.equal(foldCamera({ ...base, ownPrep: false, folded: true, current: open }), null, 'scouting / battle / observer');
assert.equal(foldCamera({ ...base, pen: true, folded: true, current: open }), null, 'the pen folds the shop itself');
assert.equal(foldCamera(null), null);
});
test('foldCamera: deferred while a piece is dragged or its direction is chosen, applied once that ends', () => {
const s = { ...base, folded: true, current: open };
assert.equal(foldCamera({ ...s, busy: true }), null, 'drag / wheel in progress: the camera stays');
assert.deepEqual(foldCamera({ ...s, busy: false }), folded, 'the effect runs again when busy clears');
});
test('foldCamera: the Final Assault prep half follows the fold too', () => {
const want = foldCamera({ ...base, pub: pubAt(14), myId: 'b', folded: true, current: { kind: 'bossPrep', opts: { side: 'R', shop: true } } });
assert.deepEqual(want, { kind: 'bossPrep', opts: { side: 'R', shop: false } });
});
});
// ---- the folded shop's HUD band (ui/fieldHost.js hudBands) ----------------------------------------------------------
/** Root font size of css/theme.css: clamp(40px, min(100vw / 19.2, 100vh / 10.8), 240px). */
const remAt = (w, h) => Math.max(40, Math.min(w / 19.2, h / 10.8, 240));
/**
* Run `fn` with a stubbed DOM: root font size, the HUD layer's rect (safe-area insets), the corner buttons (client-px
* rects, or none) and whether the pointer is coarse (`sp-coarse`, --tap-min 44 px).
*/
function withDom({ rem, hudTop = 0, hudBottom = null, corner = null, coarse = false }, fn) {
const g = globalThis;
const saved = { document: g.document, getComputedStyle: g.getComputedStyle };
const root = { classList: { contains: (c) => c === 'sp-coarse' && coarse } };
g.getComputedStyle = (el) => ({ fontSize: `${rem}px`, getPropertyValue: (p) => (el === root && p === '--tap-min' ? ' 44px' : '') });
g.document = {
documentElement: root,
querySelector: (s) => (s === '.gm__hud' ? { getBoundingClientRect: () => ({ top: hudTop, bottom: hudBottom ?? 0 }) } : null),
querySelectorAll: (s) => (s.includes('.gm__corner') && corner ? corner.map((r) => ({ getBoundingClientRect: () => r })) : []),
};
try { return fn(); } finally { g.document = saved.document; g.getComputedStyle = saved.getComputedStyle; }
}
/** A one-row corner of `n` buttons `size` px high at bottom .24rem (css/screens/game.css .gm__corner). */
const cornerRow = (h, rem, size, n = 4) => Array.from({ length: n }, () => ({ top: h - 0.24 * rem - size, height: size }));
describe('hudBands: the folded shop\'s band', () => {
test('HUD_REM mirrors the CSS of the folded tab and the corner', () => {
const shop = read('public/css/screens/game-shop.css');
const game = read('public/css/screens/game.css');
// tab: bottom .2rem + padding .08rem × 2 + the .44rem button + 1 px + 2 px borders
assert.match(shop, /\.shopbar-tab \{ position: absolute; right: \.26rem; bottom: \.2rem;[^}]*padding: \.08rem;[^}]*border: 1px solid var\(--line-2\); border-top: 2px solid var\(--mint-700\); \}/);
assert.match(shop, /\.shopbar-tab__btn \{[^}]*height: \.44rem;/);
assert.equal(HUD_REM.shopTabTop, 0.2 + 0.08 * 2 + 0.44);
assert.equal(HUD_REM.shopTabBorderPx, 3);
// corner (fallback when it cannot be measured): bottom .24rem + a .56rem desktop row
assert.match(game, /\.gm__corner \{ position: absolute; left: \.24rem; bottom: \.24rem;/);
assert.match(game, /\.gm__gear \{\n {2}width: \.56rem; height: \.56rem;/);
assert.ok(Math.abs(HUD_REM.cornerTop - (0.24 + 0.56)) < 1e-9);
// folded, the narrow phones' corner is one row again (the bar that made it wrap is gone)
assert.match(read('public/css/devices.css'), /@media \(max-width: 767px\) \{\n {2}\.gm\.is-collapsed \.gm__corner \{ flex-wrap: nowrap; width: auto; \}\n\}/);
});
test('prep / Final Assault prep only; open = the bar (unchanged), folded = the higher of the tab and the corner', () => {
withDom({ rem: 40 }, () => {
for (const k of ['normal', 'unite', 'boss', 'pen']) assert.equal(hudBands(k, { width: 844, height: 390 }, { shop: false }), null, k);
for (const k of ['prep', 'bossPrep']) {
const open = hudBands(k, { width: 844, height: 390 });
assert.deepEqual(hudBands(k, { width: 844, height: 390 }, { shop: true }), open);
assert.ok(Math.abs(open.bottom - 108.6) < 1e-9, 'the bar: 2.64rem + 3 px');
const f = hudBands(k, { width: 844, height: 390 }, { shop: false });
assert.equal(f.top, open.top, 'the top band does not change');
assert.ok(Math.abs(f.bottom - 35) < 1e-9, `no corner on the page: the tab .8rem + 3 px (${f.bottom})`);
}
});
withDom({ rem: 100 }, () => assert.deepEqual(hudBands('prep', { width: 1920, height: 1080 }, { shop: false }), { top: 216, bottom: 83 }));
});
test('the corner buttons are measured, touch hit areas included; the tab sits above the bottom safe-area inset', () => {
const h = 390, rem = 40;
// desktop-like pointer: the buttons' own top
withDom({ rem, corner: cornerRow(h, rem, 34) }, () => assert.ok(Math.abs(hudBands('prep', { width: 844, height: h }, { shop: false }).bottom - (9.6 + 34)) < 1e-9));
// touch: a 44 px hit area centred on a 34 px button reaches 5 px higher
withDom({ rem, corner: cornerRow(h, rem, 34), coarse: true }, () => assert.ok(Math.abs(hudBands('prep', { width: 844, height: h }, { shop: false }).bottom - (9.6 + 34 + 5)) < 1e-9));
// two rows (a narrow phone with the bar open would wrap them): the higher row decides
const two = [...cornerRow(366, rem, 34, 1), { top: 366 - 9.6 - 34 - 4 - 34, height: 34 }];
withDom({ rem, corner: two, coarse: true }, () => assert.ok(Math.abs(hudBands('prep', { width: 756, height: 366 }, { shop: false }).bottom - (9.6 + 72 + 5)) < 1e-9));
// a low corner: the tab wins; a notched phone's bottom inset lifts the tab (it lives inside the HUD layer)
withDom({ rem, corner: cornerRow(h, rem, 10) }, () => assert.ok(Math.abs(hudBands('prep', { width: 844, height: h }, { shop: false }).bottom - 35) < 1e-9));
withDom({ rem, hudBottom: h - 21, corner: cornerRow(h - 21, rem, 10) }, () => assert.ok(Math.abs(hudBands('prep', { width: 844, height: h }, { shop: false }).bottom - (21 + 35)) < 1e-9));
// clamped like the bar's band
withDom({ rem, corner: [{ top: 10, height: 34 }] }, () => assert.equal(hudBands('prep', { width: 640, height: 200 }, { shop: false }).bottom, 80));
});
});
// ---- the camera itself (render/projection.js presetCamera with the folded band) ---------------------------------------
describe('the folded board grows and every bench / temp tile stays clear of the folded HUD', () => {
const stage = JSON.parse(read('data/stages.json')).act2autochess_m01;
const H = { h: 0.42, '#': 0.3, X: 0.55, a: 0.16, A: 0.16 }; // render/style.js TILE_H by glyph
const heightAt = (r, c) => H[stage.rows[r]?.[c]] ?? 0;
const tileAt = (cam, row) => cam.project(5.5, row - 0.5).x - cam.project(4.5, row - 0.5).x;
/** Screen y extent of the bench (row 7 / boss row 0), temp (8 / 1) and back row (12 / 5) tile tops. */
function extents(cam, rows) {
const ys = (r, c0, c1) => { const out = []; for (let c = c0; c <= c1; c++) for (const p of tileQuad(cam, r, c, heightAt(r, c))) out.push(p.y); return out; };
return { benchBottom: Math.max(...ys(rows.bench, 0, 9)), tempBottom: Math.max(...ys(rows.temp, 4, 8)), backTop: Math.min(...ys(rows.back, 4, 10)) };
}
// [w, h, coarse pointer, corner rows when folded (css/devices.css: one row again)]
const VIEWPORTS = [[1920, 1080, false], [1280, 720, false], [1680, 1050, false], [844, 390, true], [756, 366, true], [800, 360, true], [915, 412, true], [932, 430, true]];
test('fold → shop-collapsed camera (bigger tiles), bench above the band, back row under the bond strip; unfold → the shop camera', () => {
for (const [w, h, coarse] of VIEWPORTS) {
const rem = remAt(w, h);
const btn = coarse ? Math.max(0.56 * rem, 34) : 0.56 * rem;
for (const [kind, rows, opts] of [['prep', { bench: 7, temp: 8, back: 12 }, { rect: { ...GEO.NORMAL_RECT }, side: 'L' }],
['bossPrep', { bench: 0, temp: 1, back: 5 }, { side: 'L' }], ['bossPrep', { bench: 0, temp: 1, back: 5 }, { side: 'R' }]]) {
const dom = { rem, corner: cornerRow(h, rem, btn), coarse };
const openHud = withDom(dom, () => hudBands(kind, { width: w, height: h }, { shop: true }));
const foldHud = withDom(dom, () => hudBands(kind, { width: w, height: h }, { shop: false }));
const openCam = presetCamera(kind, { width: w, height: h }, { ...opts, shop: true, hud: openHud });
const foldCam = presetCamera(kind, { width: w, height: h }, { ...opts, shop: false, hud: foldHud });
const tag = `${kind}${opts.side} ${w}×${h}`;
const midRow = rows.back - 2;
assert.ok(tileAt(foldCam, midRow) > tileAt(openCam, midRow) * 1.15, `${tag}: grows (${tileAt(openCam, midRow).toFixed(1)} → ${tileAt(foldCam, midRow).toFixed(1)})`);
const e = extents(foldCam, rows);
// (projection.js clearHud keeps an official camera that overlaps a band by ≤ 0.5 px: float noise)
assert.ok(e.benchBottom <= h - foldHud.bottom + 0.5 + 1e-6, `${tag}: bench ${e.benchBottom} above the folded band ${h - foldHud.bottom}`);
assert.ok(e.tempBottom < e.benchBottom, `${tag}: temp row above the bench`);
assert.ok(e.backTop >= foldHud.top - 1e-6, `${tag}: back row ${e.backTop} under the bond strip ${foldHud.top}`);
// unfolding asks for exactly the shop camera again
assert.deepEqual(presetCamera(kind, { width: w, height: h }, { ...opts, shop: true, hud: openHud }).params(), openCam.params());
}
}
});
test('1920×1080: the official shop-collapsed camera at its own scale, panned up just enough to clear the folded tab', () => {
const off = presetCamera('prep', { width: 1920, height: 1080 }, { rect: { ...GEO.NORMAL_RECT }, side: 'L', shop: false });
const fit = presetCamera('prep', { width: 1920, height: 1080 }, { rect: { ...GEO.NORMAL_RECT }, side: 'L', shop: false, hud: { top: 216, bottom: 83 } });
const shop = presetCamera('prep', { width: 1920, height: 1080 }, { rect: { ...GEO.NORMAL_RECT }, side: 'L', hud: { top: 216, bottom: 267 } });
const { cy: cyFit, ...restFit } = fit.params();
const { cy: cyOff, ...restOff } = off.params();
assert.deepEqual(restFit, restOff, 'same optics and scale (a 2D pan of the image)');
assert.ok(cyFit < cyOff && cyOff - cyFit < 60, `panned up ${cyOff - cyFit} px`);
assert.ok(Math.abs(tileAt(fit, 10) - 130.7) < 0.2, `≈ 131 px per tile at row 10 (${tileAt(fit, 10)})`);
assert.ok(Math.abs(tileAt(shop, 10) - 109.8) < 0.2, `the shop camera: ≈ 110 px (${tileAt(shop, 10)})`);
});
});
describe('wiring', () => {
test('the view passes the request\'s shop state to the HUD bands; the game asks for the camera of the fold state', () => {
const app = read('public/js/render/app.js');
assert.match(app, /hud: hudBands\(vk, sz, \{ shop: o\.shop !== false \}\),/);
assert.match(app, /return opts\.hud\(kind, sz, o\) \|\| null;/);
const game = read('public/js/screens/game.js');
assert.match(game, /const shopFolded = showShop && \(pen \? penRef\.current\.collapsed : collapsed\);/);
// entering prep and the boss-prep re-frame take the fold state; a fold / unfold re-frames through foldCamera
assert.equal((game.match(/const pc = prepCameraFor\(pub, myId, shopFolded\);/g) || []).length, 2);
assert.match(game, /const next = foldCamera\(\{\n\s+pub, myId, folded: shopFolded, ownPrep: !!view && viewModeRef\.current === 'prep' && showPrep,\n\s+pen, busy: !!drag \|\| !!facing, current: camRef\.current,\n\s+\}\);\n\s+if \(next\) setCam\(next\.kind, next\.opts\);\n\s+\}, \[view, shopFolded, showPrep, pen, !!drag, !!facing, prepCamKey\]\);/);
});
});
+41 -1
View File
@@ -11,7 +11,7 @@
// item onto a unit (equip), 机变 pick, band pick, briefing ready, emote, exit → 暂离 → 返回. // item onto a unit (equip), 机变 pick, band pick, briefing ready, emote, exit → 暂离 → 返回.
// 3. Regressions: tooltip after a click, toasts / reconnect banner placement, refused watch targets, battle unit // 3. Regressions: tooltip after a click, toasts / reconnect banner placement, refused watch targets, battle unit
// panel closing at the next prep, no Hidden Core medal before R15, equipment dropped on an operator's upper body // panel closing at the next prep, no Hidden Core medal before R15, equipment dropped on an operator's upper body
// (engine), illegal drops toasting their reason. // (engine), illegal drops toasting their reason, the folded shop's prep camera (public issue #5).
import { test, describe, before, after } from 'node:test'; import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
@@ -518,6 +518,46 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set
await page.close(); await page.close();
}); });
for (const [w, h] of [[1920, 1080], [844, 390]]) {
test(`fold (public issue #5): the own prep board grows with the shop folded, every bench pad stays free, unfolding returns (${w}×${h})`, async (t) => {
if (RENDER !== 'engine') { t.skip('SP_RENDER=fallback'); return; }
const { page, problems } = await open('phase=PREP', { w, h });
await page.waitForFunction(() => !!globalThis.__SP_VIEW__?.raw?.debug?.cam, { timeout: 15000 });
await sleep(900);
const look = () => page.evaluate(() => {
const V = globalThis.__SP_VIEW__; const raw = V.raw; const cam = raw.debug.cam;
const tile = cam.project(5.5, 9.5).x - cam.project(4.5, 9.5).x;
let worst = 1;
for (let c = 0; c <= 9; c++) {
const q = V.tileScreen(7, c);
if (!q) continue;
const cx = q.poly.reduce((s, p) => s + p[0], 0) / 4, cy = q.poly.reduce((s, p) => s + p[1], 0) / 4;
const pts = [[cx, cy], ...q.poly.map(([x, y]) => [x + (cx - x) * 0.12, y + (cy - y) * 0.12])];
const free = pts.filter(([x, y]) => document.elementFromPoint(x, y) === raw.debug.app.view).length / pts.length;
worst = Math.min(worst, free);
}
return { tile, worst, kind: raw.debug.camKind, params: JSON.stringify(cam.params()) };
});
const open1 = await look();
await page.click('.funds__collapse');
await page.waitForSelector('.shopbar-tab', { timeout: 3000 });
await sleep(1200); // the 0.75 s camera flight
const folded = await look();
assert.equal(folded.kind, 'prep');
assert.ok(folded.tile > open1.tile * 1.15, `tile ${open1.tile.toFixed(1)} → ${folded.tile.toFixed(1)} px`);
assert.equal(folded.worst, 1, 'every bench pad fully pressable under the folded camera');
await page.screenshot({ path: path.join(OUT, `ui-prep-folded-${w}.png`) });
await page.click('.shopbar-tab__btn');
await page.waitForSelector('.shopbar', { timeout: 3000 });
await sleep(1200);
const back = await look();
assert.equal(back.params, open1.params, 'unfolded: exactly the shop camera again');
assert.equal(back.worst, 1);
assert.deepEqual(problems, []);
await page.close();
});
}
for (const [w, h] of [[1920, 1080], [1366, 768]]) { for (const [w, h] of [[1920, 1080], [1366, 768]]) {
test(`toasts clear the top bar capsule; the reconnect banner clears the view switcher (${w}×${h})`, async () => { test(`toasts clear the top bar capsule; the reconnect banner clears the view switcher (${w}×${h})`, async () => {
let { page, problems } = await open('phase=PREP', { w, h }); let { page, problems } = await open('phase=PREP', { w, h });
+2 -1
View File
@@ -148,7 +148,8 @@ describe('9: the prep camera keeps the bench clear of the shop bar on phones in
test('wiring: the game hands hudBands to the view, the view to the prep cameras', () => { test('wiring: the game hands hudBands to the view, the view to the prep cameras', () => {
assert.match(read('public/js/ui/fieldHost.js'), /padding: hudPadding, hud: hudBands \}/); assert.match(read('public/js/ui/fieldHost.js'), /padding: hudPadding, hud: hudBands \}/);
const app = read('public/js/render/app.js'); const app = read('public/js/render/app.js');
assert.match(app, /hud: hudBands\(vk, sz\),/); // (the folded shop's band since public issue #5: test/ui/issue5-fold-camera.test.js)
assert.match(app, /hud: hudBands\(vk, sz, \{ shop: o\.shop !== false \}\),/);
assert.match(app, /presetCamera\('prep', \{ width: s0\.width, height: s0\.height, padding: defaultPadding\('prep', s0\) \}, \{ hud: hudBands\('prep', s0\) \}\)/); assert.match(app, /presetCamera\('prep', \{ width: s0\.width, height: s0\.height, padding: defaultPadding\('prep', s0\) \}, \{ hud: hudBands\('prep', s0\) \}\)/);
}); });