feedback1 WD: form changes survive the client's catch-up frames, hidden tab and pre-entry buffer

Every setForm fx now carries 'form' (shared/protocol.js fxForm). The runner keeps
these fx in catch-up frames (keepsState), holds the state-bearing events of the
battle on screen while the tab is hidden and delivers them with the first frame
back (re-entering from the field meta past HELD_MAX); screens/game.js buffers
them before a field is entered. A husk beaten while its stand-up clip plays dies
on the husk's clip. 幽灵 form cited from PRTS; 特战术师 flyer question recorded.
This commit is contained in:
sganggs committed 2026-10-03 04:27:24 +08:00
1 parent e1ce0316dd
commit 2591b8638e
11 files changed
+382 -47

No files matched your search

+6 -5
View File
@@ -203,11 +203,12 @@ A skill clip may also come from directional-only animations when a model has no
The resolver's full precedence list is in the header of `tools/assets/anim-roles.mjs`.
The manifest roles describe an enemy's first form. Enemies whose skeleton holds another form's clip set get it from
`public/js/render/units.js FORMS` (keyed by Spine id, switched by the sim's 'phase' fx kind or the `form` of its
'ember' / 'revive' / 'telegraph' / 'stone' fx — or, for a view built mid-battle, UnitInfo `form`, which
`render/app.js renderInfo` passes to the view; a `change` clip plays once first, an `end` clip is timed from the fx's
`dur` to finish as that state ends). A blocked or revealed 隐匿 enemy is drawn solid: the sim sends the stealth bit only
while its 隐匿 is on:
`public/js/render/units.js FORMS` (keyed by Spine id, switched by the `form` of the sim's 'phase' / 'ember' / 'revive'
/ 'telegraph' / 'stone' fx — `shared/protocol.js fxForm`; the client runner keeps these fx through catch-up frames and
hidden tabs — or, for a view built mid-battle, UnitInfo `form`, which `render/app.js renderInfo` passes to the view; a
`change` clip plays once first, an `end` clip is timed from the fx's `dur` to finish as that state ends, keeping the
current form's death clip until the next form's fx). A blocked or revealed 隐匿 enemy is drawn solid: the sim sends the
stealth bit only while its 隐匿 is on:
- 掠海漂移体's crawl (`Change`, then `*_02`);
- 转译基底·α's three forms (`A_Die_B` / `_C` / `_D`, 2 s each, then `B_*` 寻仇者, `C_*` 幽灵, `D_*` 特战术师);
- the 深池逐火 embers (`Die`, then `Idle_2` / `Move_2` / `Die_2`; `Revive` ends as it stands up) and 假想敌:再生's puppet
+8 -4
View File
@@ -245,9 +245,12 @@ still standing after `Revive[Trigger].interval` s stands up again with full HP),
instance or a block starts a 2 s change). Each form change goes through `setForm(b, e, form, fxKind, params)`: the
unit keeps it (`e.form`, published as UnitInfo `form`, so a view built mid-battle from `fieldMeta()` — a watched
teammate's field, 联防 observers, a reconnect — starts in it: `render/app.js renderInfo` hands it to the view) and the
fx announces it: the `kind` of a form 'phase' fx
(crawl, translator_*), else the `form` of the 'ember' ('husk'), 'revive' ('revived' / 'form2' / 'fly'), 'telegraph'
('reborn') or 'stone' ('stone') fx — render/units.js FORMS. Barrier / charge 'phase' kinds are no forms.
fx announces it as its `form`: a 'phase' fx (crawl, translator_* — also its `kind`), 'ember' ('husk'), 'revive'
('revived' / 'form2' / 'fly'), 'telegraph' ('reborn') or 'stone' ('stone') — render/units.js FORMS. Barrier / charge
'phase' kinds carry no `form` and are no forms. A form is state, not decoration (b.snap tuples carry none): the client
keeps every fx with a `form` (`shared/protocol.js fxForm`) where it drops other events — the runner's catch-up frames
and its hidden-tab backlog (`battle/runner.js keepsState`), the game screen's events buffered before a field is entered
(`screens/game.js`); dropping them was report #5's look again after a stall or a background tab.
**Ownership** (`enemy.ownerId`, used for `killed/total` and leak attribution): `ownerPlayerId` if given, else the
player whose half contains the spawn tile (cols ≥ 11 = right half / player with colOffset 8 or side R). A leak is
@@ -978,7 +981,8 @@ Unknown subprofessions fall back to the profession default (test `professions.te
renderer flies it back to the thrower at `BOOMERANG_RETURN_SPEED`; an enemy's `profile.shot` may name another kind,
e.g. `mortar` for 帝国炮火先兆者, which the renderer does not draw — its fx `bombardShell` is the shell), `['dmg', tgt, amount, type]` (`phys|arts|true|burn|neural|necrosis|apoptosis`),
`['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),
`['fx', kind, x, y, extra]` (`hitCap` `{ id, n }`: a leader's hit cancelled by 限伤 — the renderer draws nothing;
`extra.form` = the enemy's model form from then on, `content/enemies.js setForm` / `shared/protocol.js fxForm`),
`['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?, form?, skillIndex? }` (`skillIndex`: an ally's equipped skill, DESIGN §16; `form`: an enemy's current model form — `content/enemies.js setForm` — so a view built mid-battle from `fieldMeta()` starts on that clip set)
(`dir` = the unit direction, allies meaningful, enemies 'RIGHT'; `facing` = its horizontal sign for sprite flipping)