From e7d957b27c5b4b1982ab46f88b3cfc3e7d30c4ec Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 19:17:27 +0800 Subject: [PATCH] feedback1 final: the loadout screen's background layer draws again (pointed out in PR #14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit css/screens/loadout.css: `.lo > * { position: relative }` (one class, a later rule) overrode `.lo__bg { position: absolute }`, so the mint glow / grid layer was a 0-px flex item and never rendered. `.lo > :where(:not(.lo__bg))` keeps the one-class specificity for every other layer and never matches the background. Checked in headless Chrome at 1920x1080 and 844x390 (touch): the layer now covers the overlay (0 px tall before), every other box of the screen is unchanged, no console errors. Tests: test/ui/loadout.test.js (the cascade; fails on the old rule), opt-in test/ui/loadout.e2e.test.js (the layer's box and stacking at both sizes). DESIGN §21.25 credits PR #14 and PR #10 (crossOrigin on the two new Image() loads); CHANGELOG 0.1.1 lines for both. --- CHANGELOG.md | 2 ++ docs/DESIGN.md | 4 +++- public/css/screens/loadout.css | 5 ++++- test/ui/loadout.e2e.test.js | 20 ++++++++++++++++++++ test/ui/loadout.test.js | 13 +++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70f70601..05269be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ - 干员详情卡的攻击范围小图实时显示技能期间的范围;整备期棋盘上的范围提示、转向轮盘和详情卡显示同一个范围。 - 网页卡顿、切到后台或中途进场观战时,不会再漏掉敌人的变身,也不会在切回来后补播早已结束的变身动画。 - 灼热 / 炽焰源石虫:整合包附带官方模型;从源码运行且没有提取本机客户端素材时,显示染成橙色 / 红橙色的源石虫。 +- 干员调配界面的背景(薄荷绿光晕和网格)恢复显示:一条样式规则把它压成了 0 像素高,界面其他部分位置不变(PR #14 指出)。 +- 素材放在其他域名(CDN、对象存储)时,图片按跨域方式加载,地图和 3D 棋盘不再显示为空白(PR #10)。 - 变形同构体的详情卡列出天赋栏里的对应关系:每个盟约由哪些装备搭配,本局禁用的盟约会标出;装在干员身上时高亮正在生效的那一组,各盟约装备的详情也写明与变形同构体一同装备时携带者视为哪个盟约的成员。 - 调和让核心盟约人数 +1 时,盟约详情写明「含调和 +1」,成员列表上方列出带来这 1 人的调和干员(如缪尔赛思);查看队友的盟约时也一样。 - 选择策略时,围绕本局禁用盟约的策略(标准模拟下的潘格尼尼、克莱门莎、玛恩纳)标出「本局禁用」并在详情里说明,仍可选择。 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 8a36101a..83a196d1 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1406,10 +1406,12 @@ Not done (noted, no primary source or out of scope): whether 华法琳 S2's 流 - **QA after the integration, fixed**: rule 3 counted only the board pieces that were not removed, but a summon leaves its home free only once it has expired or been killed (a docked one keeps its tile reserved), so the 召唤物 clause never applied — a 突袭 member that landed on the home of 浊心斯卡蒂's expired 海嗣 and fell there lay on it and kept the 海嗣 (远古血亲) from coming back. Every board piece's home counts now, on the field or not. Cost: the body bookkeeping added about 2.5 % per tick on 519 real specs (8 % on boss fields; whole matches and the perf budgets unchanged); `_checkRedeploys` now checks the DP before the tile scans, so an operator waiting for DP costs no ally scan — on the same 519 specs, against the integration head, the same tick counts and boss fields 11.9 → 10.9 µs per tick (a tile → body map is left for later). - [ASSUMED]: §21.20. Tests: `test/sim/feedback1f-downed-tile.test.js` (rule 3 for a summon that has expired or been killed), `test/render/feedback1f-down-tile.test.js`, opt-in `test/render/feedback1f-down-tile.browser.test.js`; `downelem`, `bonds_addon`, `engine-requests`, `robustness` follow the rule. -### 21.25 Hardening credited to pull requests (PR #2, PR #7) — `match/effectsMeta.js dispatch / stillEquipped`, `tools/fetch-assets.mjs shrinkGuard`, `tools/assets/manifest.mjs droppedEntries` +### 21.25 Hardening credited to pull requests (PR #2, PR #7, PR #10, PR #14) — `match/effectsMeta.js dispatch / stillEquipped`, `tools/fetch-assets.mjs shrinkGuard`, `tools/assets/manifest.mjs droppedEntries`, `assets.js loadImageElement`, `ui/guide.js preload`, `css/screens/loadout.css` - **The prep dispatcher's equipped items** (pointed out in PR #2, whose 突变细胞 bench rule §21.1 now follows — official footage confirmed it —, its merge path excepted): step 5 iterated `holder.items` live while handlers move and destroy pieces — normal 博士投影 destroys itself at the round start (the splice skipped the next item on that holder), 突变细胞 transforms its holder (the next item ran with a holder that was gone). It now walks a snapshot of the owned chess and of each holder's items and runs an item only while it is still equipped on its still-owned holder; a piece gained or equipped meanwhile waits for the next dispatch (effectsMeta header, META §2.2). No shipped item has a second hook on those events, so play is unchanged; a synthetic spy handler pins the rule (`test/match/feedback1-dispatch-snapshot.test.js`). QA after the integration: the items were read per holder as the walk reached it, so an item equipped meanwhile onto a holder not yet walked ran in the same dispatch and an item moved to a later holder ran twice (latent: no shipped item equips or moves items on those hooks); every [holder, item] pair is now taken before the first item runs, and two more synthetic cases pin it. - **fetch-assets never shrinks the committed manifest by accident** (pointed out in PR #7, whose `data/assets.json`, regenerated on a machine where some downloads failed, was 42 audio entries short — all 42 still resolve upstream): a rebuilt manifest leaves out every entry whose files are missing on that machine. When it would lose an entry of the current `data/assets.json`, the run keeps the current file, prints the entries it would drop (also the report's `droppedEntries` / `manifestWritten`) and exits 1; `--allow-shrink` (or `--prune`) writes it (ASSETS.md; `test/fetch-assets-shrink.test.js`). +- **crossOrigin on the two `new Image()` loads** (PR #10, merged on GitHub): with `/assets/` served from another origin (a CDN or object storage) an image loaded without CORS approval taints the canvas and breaks the WebGL textures (an empty map, a white 3D board). `assets.js loadImageElement` — the asset store's only image path, §21.27's seeded store included — and `ui/guide.js preload` set `crossOrigin = 'anonymous'`; a same-origin page is unaffected. +- **The loadout screen's background layer** (pointed out in PR #14): `css/screens/loadout.css` `.lo > * { position: relative }` — one class, a later rule — overrode `.lo__bg { position: absolute }`, so the mint glow and grid layer was a 0-px flex item and never drew. Now `.lo > :where(:not(.lo__bg))`: the same one-class specificity for every other layer, never the background. Checked in headless Chrome at 1920×1080 and 844×390 (touch): the layer covers the overlay and every other box of the screen is where it was (to 0.1 px). Tests: `test/ui/loadout.test.js` (the cascade), opt-in `test/ui/loadout.e2e.test.js` (`SP_E2E=1`: the layer's box and stacking at both sizes). ### 21.26 Gaps found while triaging GitHub issues #1 / #8 (v0.1.1) diff --git a/public/css/screens/loadout.css b/public/css/screens/loadout.css index 27946847..ef2cda77 100644 --- a/public/css/screens/loadout.css +++ b/public/css/screens/loadout.css @@ -26,7 +26,10 @@ linear-gradient(90deg, rgba(78, 216, 175, .035) 1px, transparent 1px) 0 0 / .6rem .6rem, linear-gradient(180deg, #0f1312, #090b0a); } -.lo > * { position: relative; } +/* every layer but the background stacks above it. :where() keeps this at the specificity of the old `.lo > *` (one + class), so the rules after it still win where they did, and it never matches .lo__bg: as `.lo > *` (same specificity, + later rule) it beat the layer's `position: absolute`, which left a 0-px tall flex item that never drew (PR #14). */ +.lo > :where(:not(.lo__bg)) { position: relative; } /* ---- top bar ---------------------------------------------------------------------------------------------------- */ .lo-top { diff --git a/test/ui/loadout.e2e.test.js b/test/ui/loadout.e2e.test.js index 13feb886..58191b9c 100644 --- a/test/ui/loadout.e2e.test.js +++ b/test/ui/loadout.e2e.test.js @@ -68,6 +68,24 @@ describe('干员调配 overlay (real server, headless Chrome)', { skip: !ENABLED while (Date.now() - t0 < timeout) { last = await st(page); if (pred(last)) return last; await sleep(150); } throw new Error(`timed out waiting for ${what}: ${JSON.stringify(last)}`); } + /** The overlay's background layer (css/screens/loadout.css .lo__bg — the mint glow and the grid): out of the flex flow + * and as large as the overlay, every other layer stacked above it. It used to be a 0-px flex item: `.lo > *` (same + * specificity, a later rule) overrode its position: absolute (PR #14). */ + const bgLayer = (page) => page.evaluate(() => { + const lo = document.querySelector('.lo'); + const el = lo.querySelector(':scope > .lo__bg'); + const a = lo.getBoundingClientRect(), b = el.getBoundingClientRect(); + return { + position: getComputedStyle(el).position, first: lo.firstElementChild === el, rect: [b.x, b.y, b.width, b.height], lo: [a.x, a.y, a.width, a.height], + others: [...lo.children].filter((c) => c !== el).map((c) => getComputedStyle(c).position), + }; + }); + const assertBgLayer = async (page) => { + const bg = await bgLayer(page); + assert.equal(bg.position, 'absolute', `the background layer is out of the flex flow ${JSON.stringify(bg)}`); + assert.ok(bg.first && bg.rect[3] > 0 && JSON.stringify(bg.rect) === JSON.stringify(bg.lo), `it covers the whole overlay ${JSON.stringify(bg)}`); + assert.ok(bg.others.length >= 3 && bg.others.every((p) => p === 'relative'), `every other layer stacks above it ${JSON.stringify(bg)}`); + }; const clickSel = async (page, sel) => { await page.waitForSelector(sel, { visible: true, timeout: 10000 }); await page.click(sel); @@ -78,6 +96,7 @@ describe('干员调配 overlay (real server, headless Chrome)', { skip: !ENABLED await clickSel(page, '.lobby-screen [data-testid="loadout-open"]'); await page.waitForSelector('.lo .lo-card', { visible: true, timeout: 15000 }); assert.equal(await page.$$eval('.lo-card', (els) => els.length), 112, 'every visible chess'); + await assertBgLayer(page); await page.screenshot({ path: path.join(OUT, 'loadout-desktop.png') }); // search → one card await page.type('.lo-search input', '隐现'); @@ -210,6 +229,7 @@ describe('干员调配 overlay (real server, headless Chrome)', { skip: !ENABLED const { ctx, page, problems } = await open({ w: 844, h: 390, touch: true }); await page.tap('.lobby-screen [data-testid="loadout-open"]'); await page.waitForSelector('.lo .lo-card', { visible: true, timeout: 15000 }); + await assertBgLayer(page); await page.screenshot({ path: path.join(OUT, 'loadout-phone.png') }); // review fix: the shared Button / TextField shrink to 6–7 px text on phones — the overlay's own controls stay readable // and tappable (返回, 全部恢复默认, the detail's 恢复默认, the search input) diff --git a/test/ui/loadout.test.js b/test/ui/loadout.test.js index 662cd5c6..62914b3d 100644 --- a/test/ui/loadout.test.js +++ b/test/ui/loadout.test.js @@ -300,6 +300,19 @@ test('untimed phases show no countdown: the "无倒计时" placeholder of Countd assert.ok(/'无倒计时'/.test(fn) || /return null/.test(fn), 'Countdown marks (or skips) the untimed state'); }); +test('the background layer (.lo__bg: mint glow + grid) keeps position: absolute — no later rule of the same specificity overrides it (PR #14)', () => { + const css = readFileSync(path.join(ROOT, 'public/css/screens/loadout.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, ''); + assert.match(css, /\.lo__bg\s*\{[^}]*position:\s*absolute;/); + // `.lo > *` (one class, after .lo__bg) used to set position: relative on it, a 0-px flex item that never drew + assert.ok(!/\.lo\s*>\s*\*\s*\{[^}]*position/.test(css), 'no universal child rule setting position'); + assert.match(css, /\.lo > :where\(:not\(\.lo__bg\)\) \{ position: relative; \}/, 'the other layers still stack above it, at one class of specificity'); + const after = css.slice(css.indexOf('.lo__bg')); + for (const m of after.matchAll(/([^{}]+)\{([^}]*)\}/g)) { + if (!/\.lo__bg\b/.test(m[1]) || /:not\(\.lo__bg\)/.test(m[1])) continue; + for (const v of m[2].matchAll(/position:\s*([a-z-]+)/g)) assert.equal(v[1], 'absolute', `${m[1].trim()} changes the layer's position`); + } +}); + // ---- review fixes (adversarial review of the loadout workstream) ------------------------------------------------------- /** A net whose replies are released by hand (the socket is ordered: replies come back in request order). */