From 996fede8d8fe1f791dc96beaa3810b19b7ed1866 Mon Sep 17 00:00:00 2001 From: YinqiMargatroid <206523148+YinqiMargatroid@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:02:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=AE=80=E5=8C=96=E6=A3=8B=E7=9B=98?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=A4=96=E5=8B=A4=E5=8C=BB=E7=96=97=E9=A2=84?= =?UTF-8?q?=E7=BD=AE=E8=A7=92=E8=89=B2=20(#315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/client.md | 2 +- docs/history/0.2.2.md | 4 ++-- public/js/ui/fallbackField.js | 10 ++++++++++ test/render/browser.test.js | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/design/client.md b/docs/design/client.md index 61bc511f..f3c9bc49 100644 --- a/docs/design/client.md +++ b/docs/design/client.md @@ -77,6 +77,6 @@ Target: a **real 3D board layer** rendered with three.js (vendored ESM, no CDN) - One camera model drives both layers: `public/js/render/projection.js` keeps its public API (`worldToScreen`, `screenToWorld`/tile picking, presets, transitions) but derives it from a three.js `PerspectiveCamera` initialised from the official configBlackBoard framing, so Spine units, FX, drag/drop and hit-testing stay aligned with the 3D board. - Must degrade gracefully: when local-client art or WebGL2 features are missing, fall back to the current 2D atlas board; keep 60 fps (static geometry merged into few draw calls; textures mipmapped; DPR-aware). - Implemented (`public/js/render/board3d/*`, `render/app.js`): three.js is downloaded only when the local-art manifest lists the board atlas; the built 3D area, the drawn 2D rows and the lit rect follow `viewKind(kind, {rect})` (the field the camera actually shows: a 'prep' camera on rows ≤ 6 = `bossPrep`); the background plane is extended with mirrored copies and the clear colour matches the fog (no visible edge at any aspect); battle crates live in their own group (survive scene rebuilds) and follow the board mode (`switchableBox`: 3D mesh ⇄ 2D box on a mode switch or a lost WebGL context); quality 'low' drops shadows; `?board=2d|3d` overrides. The direction wheel's range stripes sit between the board canvas and the unit canvas. -- Done: Final Assault / Hidden Core prep takes place on your own half of the boss field (research 09 §1.2): pieces shown at boss-field positions (row − 7; right-hand player mirrored col → 20 − col, RIGHT ↔ LEFT), drop targets and wheel directions mapped back to board coordinates (`prepCamera` → `bossPrep` in public/js/ui/gameLogic.js; covered by the playtest2 browser tests). A scout of such a board (`m.field` prep of a boss round) comes in boss-field coordinates with the player's `side` and is framed the same way, the leader standing at its spawn (0.2.0, community report item 55). Both players of a pair are shown together, as in the battle (0.2.0, community report item 51): the partner's board stands on the other half (`m.private.bossMate` units, already in boss-field coordinates: mirrored on the right half, RIGHT ↔ LEFT; read-only `m:` views of `render/app.js syncReadOnlyPrepUnits`), the whole boss field is lit, a scout carries both halves (`mate`), and the team panel frames the pair's avatars in the official green (`ui/battle bg_team_border` tinted, a plain ring without the local art; `gameLogic teamFrameIds`) from the round's start through the fight. With 外勤医疗 selected, every living player's own prep, paired boss prep and scouted prep shows a read-only 预备干员-医疗 on its stage slot; the strategy's Touch replacement occurs only when combat begins. +- Done: Final Assault / Hidden Core prep takes place on your own half of the boss field (research 09 §1.2): pieces shown at boss-field positions (row − 7; right-hand player mirrored col → 20 − col, RIGHT ↔ LEFT), drop targets and wheel directions mapped back to board coordinates (`prepCamera` → `bossPrep` in public/js/ui/gameLogic.js; covered by the playtest2 browser tests). A scout of such a board (`m.field` prep of a boss round) comes in boss-field coordinates with the player's `side` and is framed the same way, the leader standing at its spawn (0.2.0, community report item 55). Both players of a pair are shown together, as in the battle (0.2.0, community report item 51): the partner's board stands on the other half (`m.private.bossMate` units, already in boss-field coordinates: mirrored on the right half, RIGHT ↔ LEFT; read-only `m:` views of `render/app.js syncReadOnlyPrepUnits`), the whole boss field is lit, a scout carries both halves (`mate`), and the team panel frames the pair's avatars in the official green (`ui/battle bg_team_border` tinted, a plain ring without the local art; `gameLogic teamFrameIds`) from the round's start through the fight. With 外勤医疗 selected, every living player's own prep, paired boss prep and scouted prep shows a read-only 预备干员-医疗 on its stage slot; the strategy's Touch replacement occurs only when combat begins. The DOM fallback draws the same reserve medic from `m.private.prepMapChars` without adding a draggable piece. --- diff --git a/docs/history/0.2.2.md b/docs/history/0.2.2.md index f7641cb6..3cc1a3cd 100644 --- a/docs/history/0.2.2.md +++ b/docs/history/0.2.2.md @@ -1587,8 +1587,8 @@ Notes for the integrator describes a 预备干员-医疗 appearing on every player's field and being replaced by Touch at combat start if at least two elite operators stand there. PRTS 索敌 places the last deployed target ahead at equal taunt. [ASSUMED]: the replacement retains the map character's pre-deployment aggro position; the sources do not fix its exact numeric rank. -- **Now**: in PREP, every living player's own board and scouted board shows a read-only 预备干员-医疗 from the stage's map-character slot when any living player holds 外勤医疗; a boss pair's two medics appear on the respective halves. The prep unit does not become a movable `board` piece. At `battleStart`, only the 外勤医疗 map character (预备干员-医疗 or its Touch replacement) gets an early `aggroSeq`. Its actual deployment event and `deploySeq` stay unchanged. Operators, ordinary initial summons and mid-battle summons retain their existing priority. +- **Now**: in PREP, every living player's own board and scouted board shows a read-only 预备干员-医疗 from the stage's map-character slot when any living player holds 外勤医疗; a boss pair's two medics appear on the respective halves. The DOM fallback also draws the medic without making it draggable. The prep unit does not become a movable `board` piece. At `battleStart`, only the 外勤医疗 map character (预备干员-医疗 or its Touch replacement) gets an early `aggroSeq`. Its actual deployment event and `deploySeq` stay unchanged. Operators, ordinary initial summons and mid-battle summons retain their existing priority. - **Golden**: one value moves in `test/golden/matches.json`: `coop2-NORMAL-14-standins`, player `h_0`'s `dmgDealt` 3,000,543 → 2,999,081. That match fields 外勤医疗, so its changed initial target order can alter damage; every other recorded result is unchanged. **Tests**: `test/content/bands.test.js` checks both variants' priority and - “萨米的意志” 冰凌 selection on solo and two-player boss fields; `test/match/touch-prep.test.js` checks own, scouted and paired boss preparation views; the render browser test checks that the medic is visible, mirrored and not draggable. + “萨米的意志” 冰凌 selection on solo and two-player boss fields; `test/match/touch-prep.test.js` checks own, scouted and paired boss preparation views; the render browser test checks that both renderers show the medic, with the right-side mirror and no drag behavior. diff --git a/public/js/ui/fallbackField.js b/public/js/ui/fallbackField.js index 9e76f3c9..925a5ece 100644 --- a/public/js/ui/fallbackField.js +++ b/public/js/ui/fallbackField.js @@ -332,6 +332,16 @@ export function createFallbackView(host, opts = {}) { const now = performance.now(); if (st.mode === 'prep' && st.priv) { const heldPos = (p) => { const t = st.held.get(p.uid); return t ? tilePos(L, t.row, t.col) : null; }; + for (const u of Array.isArray(st.priv.prepMapChars) ? st.priv.prepMapChars : []) { + if (!u || !Number.isInteger(u.x) || !Number.isInteger(u.y) || typeof u.defId !== 'string') continue; + const pos = tilePos(L, u.y, u.x); + const src = tokenAvatarUrl(m(), u.defId); + pieces.push(html`
+
${src ? html`` : html`${[...(u.name || '?')][0]}`}
+
`); + } for (const p of Array.isArray(st.priv.board) ? st.priv.board : []) { if (!p || !Number.isInteger(p.row)) continue; const pos = heldPos(p) || tilePos(L, p.row, p.col); diff --git a/test/render/browser.test.js b/test/render/browser.test.js index 2f46ec04..5b38bdab 100644 --- a/test/render/browser.test.js +++ b/test/render/browser.test.js @@ -81,6 +81,7 @@ describe('render engine in headless Chrome', { skip }, () => { st.prepMapChars = [{ id: -1, uid: -1, kind: 'token', side: 'ally', ownerId: 'p_0', defId: 'char_605_cmedic', name: '预备干员-医疗', spine: 'char_605_cmedic', avatar: 'char_605_cmedic', area: 'board', x: 2, y: 10, dir: 'RIGHT', facing: 1, maxHp: 1202 }]; + window.__touchPrepChar = st.prepMapChars[0]; v.setPrep(st, { editable: true }); const medic = v.debug.views.get('m:-1'); const p = v.tileScreen(10, 2), rect = v.debug.app.view.getBoundingClientRect(); @@ -105,12 +106,27 @@ describe('render engine in headless Chrome', { skip }, () => { v.setPrep(st, { editable: true }); return !v.debug.views.has('m:-1'); }); + const fallback = await page.evaluate(async () => { + const { createFallbackView } = await import('/js/ui/fallbackField.js'); + const host = document.createElement('div'); + host.style.cssText = 'position:absolute;width:800px;height:600px'; + document.body.appendChild(host); + const ff = createFallbackView(host, { data: { get: () => null, lookup: () => null } }); + ff.setPrep({ board: [], hand: [], temp: [], prepMapChars: [window.__touchPrepChar] }, { editable: true }); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + const el = host.querySelector('.ff-piece--map-char'); + const out = { exists: !!el, name: el?.title, pointerEvents: el ? getComputedStyle(el).pointerEvents : null, + movableId: el?.dataset.uid ?? null }; + ff.destroy(); host.remove(); + return out; + }); await page.close(); assert.deepEqual(problems, []); assert.deepEqual(first, { exists: true, id: 'char_605_cmedic', x: 2, y: 10, draggable: false }); assert.deepEqual(boss, [18, 3, 'LEFT']); assert.equal(removed, true); + assert.deepEqual(fallback, { exists: true, name: '预备干员-医疗', pointerEvents: 'none', movableId: null }); }); test('fx gallery: every sim fx kind, projectile and status renders without errors; board art in use', async () => {