feedback1d WN: a stun before 暴鸰's release interrupts the cast; the release lands on tick 8; SIM.md timeline fixed

This commit is contained in:
sganggs committed 2026-10-03 05:55:29 +08:00
1 parent 47d4215cfe
commit 3cdf7182f2
3 files changed
+56 -13

No files matched your search

+5 -4
View File
@@ -214,12 +214,13 @@ enemy's attack: `enemy.profile.deferHit` = the engine makes the attack (target,
damage — the content's `attack` handler resolves it (帝国炮火先兆者's shells landing 3 s later, `content/enemies.js
kitShell`); `enemy.profile.shot` = the `'atk'` event's projectile kind (`'mortar'`: no projectile drawn). 暴鸰 (`kitBombd`,
no normal attack) drops its one bomb as a projectile: the cast (trigger: an ally within its range 2; the drone hovers
through it) releases it `BOMBD_RELEASE` (0.267 s, the Attack clip's OnAttack) later — `'atk'` kind `'droneBomb'` — and
through it) releases it `BOMBD_RELEASE` (8 ticks, the Attack clip's OnAttack on frame 8) later — `'atk'` kind
`'droneBomb'` and fx `'phase'` {kind `'bombed'`} (`unit.form` → `UnitInfo.form`; the model flies on without it) — and
it hits on arrival (target 100 % ATK, the other allies of the 8 tiles around its landing tile 100 % splash, camouflage
ignored; a target gone mid-flight: it lands where it was). The cast ends once the bomb has landed and at least
`BOMBD_POST_DELAY` (0.667 s) after the release: fx `'phase'` {kind `'bombed'`} (the model flies on without it), the
drone's speed ×2, and it moves again (feedback D4 after 0.1.0: the damage used to land in the trigger tick with the
bomb still on the drone). Crates (stage devices with
`BOMBD_POST_DELAY` (0.667 s) after the release: the drone's speed ×2, and it moves again. A stun / freeze / sleep
before the release interrupts the cast: nothing leaves the drone and the skill re-arms with its 1 s cooldown (feedback
D4 after 0.1.0: the damage used to land in the trigger tick with the bomb still on the drone). Crates (stage devices with
role `crate`, 100 HP) are ground obstacles; an enemy forced through one is blocked by it and destroys it. A device is
present when data/stages.json says `active: true` (this wins over the level file's `hidden`: act1 m02's crates);
research stages without `active` use `!hidden`. Active platforms/mounds (射击台, act1 m03) [ASSUMED, DATA §15.11] are
+25 -7
View File
@@ -86,9 +86,10 @@ const CROSS_REACH = 6;
/** 暴鸰 投弹: the target's tile and its 8 neighbours (PRTS "对目标及其周围八格的我方单位造成100%物理伤害"). */
const BOMB_REACH = 1;
/** 暴鸰 投弹: the bomb leaves on the OnAttack event of the cast's Attack clip (the official battle prefab's Boomb ability:
* animKey Attack, `_waitForAttackEvent`; the skeleton's OnAttack is at 0.267 s — data/assets.json hits.Attack). It then
* flies as projectile_bombd (`_speed` 5 = PROJECTILE_SPEEDS.droneBomb, homing, `_ignoreCamouflage`). */
export const BOMBD_RELEASE = 0.267;
* animKey Attack, `_waitForAttackEvent`; the skeleton's OnAttack is on frame 8 of the 30 fps clip — data/assets.json
* hits.Attack 0.267, rounded — so exactly 8 sim ticks). It then flies as projectile_bombd (`_speed` 5 =
* PROJECTILE_SPEEDS.droneBomb, homing, `_ignoreCamouflage`). */
export const BOMBD_RELEASE = 8 / 30;
/** 暴鸰 投弹: the cast ends once the bomb has landed and no sooner than this after the release (the Boomb ability:
* `_fireAttackFinishWhenProjectileInvalid` 1, `_minPostDelayWhenProjectileInvalid` 0.667, `_waitForAnimEndWhenProjectileInvalid`
* 0). Then its buff bomb_s (template switch_mode_restart_fsm: mode S1 + the move-speed modifier) — PRTS "技能结束后移速最终
@@ -1486,11 +1487,14 @@ function kitRoar(ab) {
* UnitInfo.form); on arrival the target (the ranged target by engine priority) takes 100 % ATK and every other ally of
* the 8 tiles around where it lands 100 % ATK splash (camouflage ignored); a target gone mid-flight: the bomb lands
* where it was. The cast ends once the bomb has landed, at least BOMBD_POST_DELAY after the release: move speed
* ×boomb.move_speed, and it flies on.
* ×boomb.move_speed, and it flies on. A stun / freeze / sleep before the release interrupts the cast (Boomb
* `_immuneStunWhenAffecting` 0, like the other enemy channels here): nothing leaves the drone, it keeps its bomb and casts
* again after the skill's cooldown (1 s, counted from the interrupt) once it is free and an operator is in range.
* [ASSUMED] no drop when the drone is dead at the release; the ATK at the release; the hover through the cast (the
* engine pauses an enemy for its attack clip, ATTACK_PAUSE; the drop is the drone's only attack-like cast); the
* bomb-less look from the release (the bomb leaves the drone on that frame of the Attack clip; at the cast end — 1 tick
* before the clip ends — the client would draw the bomb back for a frame).
* before the clip ends — the client would draw the bomb back for a frame); an interrupted cast does not use up the one
* trigger (`_maxTriggerTime` 1 — the bomb is still on the drone); a stun after the release does not stop the cast end.
*/
function kitBombd(ab) {
const s = ab.sk.boomb;
@@ -1508,8 +1512,19 @@ function kitBombd(ab) {
e.pauseUntil = b.time;
if (ms > 0) b.addBuff(e, { key: 'ab:bombRun', mods: { moveMul: ms }, persist: true });
};
// the cast before its release: { a: the skill ability, rel: the scheduled release }
let pending = null;
// a stun / freeze / sleep before the release: the bomb stays on; the skill re-arms with its cooldown
const interrupt = (b, e) => {
const { a, rel } = pending;
pending = null;
rel.cancel();
e.pauseUntil = b.time;
a.left = Math.max(TICK, num(s.cd, 1));
};
return [
{ spawn(b, e) { e.profile.noAttack = true; } },
{ spawn(b, e) { e.profile.noAttack = true; },
tick(b, e) { if (pending && e.s.flags.stun) interrupt(b, e); } },
skill(s, (b, e, a) => {
const t = byPriority(e, targetsNear(b, e, reach(e)))[0];
if (!t) return;
@@ -1519,7 +1534,10 @@ function kitBombd(ab) {
e.pauseUntil = Math.max(e.pauseUntil, b.time + BOMBD_RELEASE + 10 + BOMBD_POST_DELAY);
let landed = false, waited = false;
const done = () => { if (landed && waited) finish(b, e); };
b.after(BOMBD_RELEASE, () => {
pending = { a, rel: null };
pending.rel = b.after(BOMBD_RELEASE, () => {
if (pending && e.alive && e.s.flags.stun) { interrupt(b, e); return; } // stunned after this tick's ability pass
pending = null;
if (!e.alive) return;
const atk = e.s.atk;
b._ev(['atk', e.id, t.id, 'droneBomb']);
+26 -2
View File
@@ -9,7 +9,9 @@
// its end clip, and its buff bomb_s switches the drone to its bomb-less mode (S1: the *_2 clips) at ×2 speed
// ("技能结束后移速最终提升至200%"). Now (content/enemies.js kitBombd): the drone hovers through the cast; 'atk' kind
// 'droneBomb' + fx 'phase' { kind: 'bombed' } at the release (the view starts the *_2 clips after the Attack clip),
// damage on arrival, the speed-up when the cast ends. Also UnitInfo.form for 掠海漂移体's crawl.
// damage on arrival, the speed-up when the cast ends; a stun before the release interrupts the cast (Boomb
// `_immuneStunWhenAffecting` 0: no bomb leaves a stunned drone; it casts again once free). Also UnitInfo.form for
// 掠海漂移体's crawl.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
@@ -74,7 +76,7 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
assert.equal(atk.length, 1, 'exactly one drop (and never a normal attack)');
const [relT, relEv] = atk[0];
assert.equal(relEv[3], 'droneBomb', 'drawn as the drone\'s bomb');
assert.ok(Math.abs(relT - castAt - RELEASE) <= TICK + 1e-9, `released ${(relT - castAt).toFixed(3)} s after the trigger (OnAttack ${RELEASE})`);
assert.ok(Math.abs(relT - castAt - RELEASE) < 1e-6, `released ${(relT - castAt).toFixed(3)} s after the trigger (OnAttack, frame 8 of the Attack clip)`);
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 bomb has left the drone: its bomb-less mode from 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');
@@ -137,6 +139,28 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
assert.ok(!e.findBuff('ab:bombRun'), 'a dead drone gets no speed-up');
});
test('a stun before the release interrupts the cast: nothing leaves the drone; it casts again once free and drops its one bomb', () => {
const h = arena([{ chessId: 't_a', row: 10, col: 6 }]);
h.step();
const e = put(h, [10, 7]);
h.runUntil(() => (ability(e)?.casts ?? 0) > 0, 5);
const castAt = h.b.time;
assert.ok(h.b.applyStatus(e, 'stun', { duration: 1.5, source: null }), 'stunned mid-cast');
h.run(1.4);
const drops = () => h.events.filter((ev) => ev[0] === 'atk' && ev[1] === e.id);
assert.equal(drops().length, 0, 'no drop from a stunned drone (Boomb `_immuneStunWhenAffecting` 0)');
assert.ok(!h.events.some((ev) => ev[0] === 'fx' && ev[1] === 'phase' && ev[4]?.id === e.id), 'it keeps its bomb');
assert.equal(h.unit('t_a').stats.taken, 0);
assert.ok(!e.findBuff('ab:bombRun'), 'no speed-up');
h.runUntil(() => drops().length > 0, 3);
assert.equal(ability(e).casts, 2, 'cast again once the stun is over');
assert.ok(h.b.time >= castAt + 1.5 + RELEASE - 1e-6, 'after the stun');
h.run(2);
assert.equal(drops().length, 1, 'still one bomb in all');
assert.ok(Math.abs(h.unit('t_a').stats.taken - e.s.atk) < 1e-6, 'and it lands');
assert.ok(e.findBuff('ab:bombRun'), 'then the speed-up');
});
test('the drone killed before the release: no bomb, no mode change', () => {
const h = arena([{ chessId: 't_a', row: 10, col: 6 }]);
h.step();