fix: 简化棋盘显示外勤医疗预置角色 (#315)

This commit is contained in:
YinqiMargatroid committed 2026-10-09 23:02:43 +08:00
1 parent 9db9f3ceea
commit 996fede8d8
4 files changed
+29 -3

No files matched your search

+1 -1
View File
@@ -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:<uid>` 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:<uid>` 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.
---
+2 -2
View File
@@ -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.
+10
View File
@@ -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`<div key=${`map:${u.uid}`} class="ff-piece ff-piece--token ff-piece--map-char"
style=${`transform:translate(${pos.x}px,${pos.y}px);width:${L.tile}px;height:${L.tile}px;pointer-events:none`}
title=${u.name || u.defId}>
<div class="ff-piece__art">${src ? html`<img src=${src} alt="" draggable=${false} />` : html`<span>${[...(u.name || '?')][0]}</span>`}</div>
</div>`);
}
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);
+16
View File
@@ -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 () => {