From cc57afa8c46117184f615e2077ecdeb616b1c917 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 04:22:36 +0800 Subject: [PATCH] =?UTF-8?q?feedback1d=20WN:=20a=20field=20opened=20after?= =?UTF-8?q?=20=E6=9A=B4=E9=B8=B0's=20drop=20draws=20it=20without=20its=20b?= =?UTF-8?q?omb=20(UnitInfo.form)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bomb-less mode lived only in the client that saw the fx 'phase' event. UnitInfo now carries an enemy's recorded mode (unit.form, set by kitBombd at the release), and the renderer keeps it on the unit info, so a field watched or rejoined later starts on the *_2 clips. --- docs/SIM.md | 2 +- public/js/render/app.js | 2 ++ server/sim/content/enemies.js | 1 + server/sim/snapshot.js | 7 +++++-- test/render/feedback1d-bombd.test.js | 7 +++++++ test/sim/feedback1d-bombd.test.js | 1 + 6 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/SIM.md b/docs/SIM.md index 592442dd..0f66de58 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -963,7 +963,7 @@ Unknown subprofessions fall back to the profession default (test `professions.te `['heal', tgt, amount]`, `['skill', id, 1|0]`, `['die', id, reason]`, `['leak', id]`, `['status', id, key, 1|0]`, `['fx', kind, x, y, extra]` (`hitCap` `{ id, n }`: a leader's hit cancelled by 限伤 — the renderer draws nothing), `['layer', playerId, bondId, n]` (n = the layers actually added, capped at 999), `['bounty', playerId, coins]`. -- `UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?, skillIndex? }` (`skillIndex`: an ally's equipped skill, DESIGN §16) +- `UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?, skillIndex?, form? }` (`skillIndex`: an ally's equipped skill, DESIGN §16; `form`: an enemy's mode recorded by content as `unit.form` — 暴鸰 `'bombed'` after its drop — for views built later) (`dir` = the unit direction, allies meaningful, enemies 'RIGHT'; `facing` = its horizontal sign for sprite flipping) (`spine`/`avatar` are asset ids from data). - flags: UF bits (blocked 1, stunned 2, frozen 4, stealth 8 — 隐匿 or an ally's 迷彩 — skill 16, shield 32, invuln 64, cold 128, sleep 256, flying 512); diff --git a/public/js/render/app.js b/public/js/render/app.js index 2e145fa4..5ba7ea59 100644 --- a/public/js/render/app.js +++ b/public/js/render/app.js @@ -1296,6 +1296,8 @@ export async function createFieldView(host, options = {}) { // tap hands them to the detail card (a teammate's unit shows its owner's skill / module) skillIndex: Number.isInteger(u.skillIndex) ? u.skillIndex : undefined, moduleId: typeof u.moduleId === 'string' ? u.moduleId : undefined, + // an enemy's mode already taken (UnitInfo.form: 暴鸰 'bombed'): a field opened later draws it (units.js FORMS) + form: typeof u.form === 'string' ? u.form : undefined, }; infos.set(info.id, info); return info; diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index 1954fe4e..b456a5fa 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -1502,6 +1502,7 @@ function kitBombd(ab) { if (!e.alive) return; const atk = e.s.atk; b._ev(['atk', e.id, t.id, 'droneBomb']); + e.form = 'bombed'; // UnitInfo.form: a view built later draws it bomb-less b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'bombed' }); b.addProjectile({ from: e, target: t, speed: PROJECTILE_SPEEDS.droneBomb, visual: 'droneBomb', source: e, hitDead: true, onHit: (c) => land(b, e, atk, c.target, c.x, c.y) }); diff --git a/server/sim/snapshot.js b/server/sim/snapshot.js index 5525bfc3..49d4214d 100644 --- a/server/sim/snapshot.js +++ b/server/sim/snapshot.js @@ -1,8 +1,10 @@ // server/sim/snapshot.js — compact serialization for clients (DESIGN §8.2). // // b.snap = { fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total } -// UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, skillIndex?, moduleId? } -// dir = 'UP'|'RIGHT'|'DOWN'|'LEFT' (allies: the deploy direction, sim/dir.js); facing = its horizontal sign (±1). +// UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, skillIndex?, moduleId?, form? } +// dir = 'UP'|'RIGHT'|'DOWN'|'LEFT' (allies: the deploy direction, sim/dir.js); facing = its horizontal sign (±1); +// form = an enemy's current mode (`unit.form`, the last fx 'phase' kind content recorded — 暴鸰 'bombed'), so a view +// built after the change (a field opened mid-battle) draws it (render/units.js FORMS). // flags bits & anim codes come from shared/constants.js (UF / ANIM). import { UF, ANIM } from '../../shared/constants.js'; @@ -33,6 +35,7 @@ export function unitInfo(u) { motion: u.motion === 'FLY' ? 'FLY' : undefined, boss: u.isBoss ? true : undefined, uid: u.uid ?? undefined, + form: typeof u.form === 'string' ? u.form : undefined, // DESIGN §16: the equipped skill's index (the renderer / audio pick that skill's Spine clip and sound) skillIndex: u.side === 'ally' && Number.isInteger(d.skill?.index) ? d.skill.index : undefined, // DESIGN §16: an elite ally's equipped module (uniEquipId | 'none'; display only — a teammate's unit in a shared diff --git a/test/render/feedback1d-bombd.test.js b/test/render/feedback1d-bombd.test.js index a00f4cf1..cb937404 100644 --- a/test/render/feedback1d-bombd.test.js +++ b/test/render/feedback1d-bombd.test.js @@ -88,6 +88,13 @@ describe('D4 暴鸰: the drone lets go of its bomb on screen', () => { assert.equal(v.atkInterval, 5, 'a one-off drop leaves the attack rhythm alone'); }); + test('render/app.js keeps UnitInfo.form (a field opened after the drop) and hands the atk kind to the view', () => { + const src = readFileSync(path.join(ROOT, 'public/js/render/app.js'), 'utf8'); + assert.match(src, /function addInfo\(u\)[\s\S]{0,2000}form: typeof u\.form === 'string' \? u\.form : undefined/); + assert.match(src, /src\.onAttack\?\.\(tgt, now, e\[3\]\)/); + assert.match(src, /v\.windUp\(t - upcomingT, e\[3\]\)/); + }); + test('the drop draws a falling bomb at the sim\'s speed and the mode change draws nothing of its own', () => { assert.ok(PROJ.droneBomb, 'PROJ.droneBomb'); assert.equal(PROJ.droneBomb.speed, PROJECTILE_SPEEDS.droneBomb); diff --git a/test/sim/feedback1d-bombd.test.js b/test/sim/feedback1d-bombd.test.js index 153d2b3d..d6d36a79 100644 --- a/test/sim/feedback1d-bombd.test.js +++ b/test/sim/feedback1d-bombd.test.js @@ -70,6 +70,7 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => { assert.ok(Math.abs(relT - castAt - RELEASE) <= TICK + 1e-9, `released ${(relT - castAt).toFixed(3)} s after the trigger (OnAttack ${RELEASE})`); const phase = log.find(([, ev]) => ev[0] === 'fx' && ev[1] === 'phase'); assert.ok(phase && phase[1][4].kind === 'bombed' && Math.abs(phase[0] - relT) < 1e-9, 'the model drops to its bomb-less mode at the release'); + if (drone.alive) assert.equal(b.fieldMeta().units.find((u) => u.id === drone.id).form, 'bombed', 'a field opened later draws it bomb-less'); assert.ok(hitAt != null, 'the bomb lands'); assert.equal(firstHit.id, relEv[2], 'on the operator it was dropped on'); assert.ok(hitAt - relT > TICK, `then flies (${(hitAt - relT).toFixed(3)} s at ${PROJECTILE_SPEEDS.droneBomb} tiles/s)`);