feedback1d WN: 暴鸰 hovers through its cast and speeds up when it ends; 掠海漂移体's crawl in UnitInfo.form
- Official Boomb ability: the cast finishes once the bomb has landed and at least 0.667 s after the release (_fireAttackFinishWhenProjectileInvalid, _minPostDelayWhenProjectileInvalid), then buff bomb_s gives the speed (PRTS "技能结束后移速最终提升至200%"). kitBombd: BOMBD_POST_DELAY; the speed-up at the cast end instead of the landing; the drone holds still until then, like the engine's pause for an enemy attack clip. - The bomb-less look still starts at the release: the view begins the *_2 clips after the Attack clip (the official end clip Idle_2); at the cast end it would draw the bomb back for a frame. - kitSyufo records e.form = 'crawl', so a field opened later draws it crawling.
This commit is contained in:
1 parent
04634f5bf7
commit
47d4215cfe
5 files changed
+96
-36
No files matched your search
+8
-7
@@ -213,12 +213,13 @@ never attack; `dmgType 'heal'` enemies heal the lowest-HP% enemy in their radius
|
||||
enemy's attack: `enemy.profile.deferHit` = the engine makes the attack (target, timing, the `'atk'` event) but deals no
|
||||
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) releases it
|
||||
`BOMBD_RELEASE` (0.267 s, the Attack clip's OnAttack) later — `'atk'` kind `'droneBomb'` and fx `'phase'` {kind
|
||||
`'bombed'`}: 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), then the
|
||||
drone's speed ×2 (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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -963,7 +964,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?, 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)
|
||||
- `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` — 掠海漂移体 `'crawl'`, 暴鸰 `'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);
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
// fx kinds emitted (battle.fx(kind, {x, y, …})): 'explode' {r, kind} · 'zone' {r, dur, kind} · 'telegraph' {r, dur,
|
||||
// kind, tiles?} (delayed strikes, charges, 'reborn') · 'beam' {from, to, kind} · 'summon' {id, key} · 'ember'/'revive'/
|
||||
// 'stone' {id} · 'blink' {id, fx, fy} · 'charge' {id, tx, ty} · 'expose' {id} · 'shieldBreak' {id} · 'liberate' {id}
|
||||
// · 'phase' {id, kind} (form / barrier changes) · 'lpLoss' {value, reason} · 'steal' · 'ignite'.
|
||||
// · 'phase' {id, kind} (form / barrier changes; a mode with its own clip set — render/units.js FORMS: 掠海漂移体
|
||||
// 'crawl', 暴鸰 'bombed' — is also kept in `e.form` → UnitInfo.form) · 'lpLoss' {value, reason} · 'steal' · 'ignite'.
|
||||
// Custom hook: 'lpLoss' {amount, reason, source} — leader "扣除目标生命" effects; also summed into result.lpLoss.
|
||||
|
||||
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE, PROJECTILE_SPEEDS } from '../constants.js';
|
||||
@@ -88,6 +89,11 @@ const BOMB_REACH = 1;
|
||||
* 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;
|
||||
/** 暴鸰 投弹: 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 "技能结束后移速最终
|
||||
* 提升至200%"; the cast's end clip is already the bomb-less Idle_2 (`_endAnimKey`). */
|
||||
export const BOMBD_POST_DELAY = 0.667;
|
||||
/** 帝国炮火先兆者 shell (PRTS "普通攻击向目标所在位置发射一枚于3秒后命中的弹道,弹道对半径1.2范围内的所有我方单位造成攻击力
|
||||
* 100%的无来源物理伤害 … ※弹道始终使用缓存攻击力"): flight time and blast radius. */
|
||||
const SHELL_FLIGHT = 3, SHELL_RADIUS = 1.2;
|
||||
@@ -1082,6 +1088,7 @@ function kitSyufo(ab) {
|
||||
setFloat(b, e, false);
|
||||
e.profile.melee = true;
|
||||
b.applyStatus(e, 'stun', { duration: SYUFO_CRAWL_STUN, source: null });
|
||||
e.form = 'crawl'; // UnitInfo.form: a view built later draws it crawling
|
||||
b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'crawl' });
|
||||
},
|
||||
}];
|
||||
@@ -1473,12 +1480,17 @@ function kitRoar(ab) {
|
||||
* projectile: the cast plays the Attack clip, the bomb leaves on its OnAttack event (BOMBD_RELEASE) and flies to the
|
||||
* target (projectile_bombd), and the drone switches to its bomb-less mode (S1, buff bomb_s: the *_2 clips, no bottle).
|
||||
* User feedback after 0.1.0 (D4 "炸弹无法正常投放"): the damage used to land in the tick of the trigger while the drone
|
||||
* kept its bomb on screen. Now: at the release an 'atk' event of kind 'droneBomb' (the client winds the Attack clip up
|
||||
* to it and flies the bomb) and fx 'phase' {kind: 'bombed'} (render FORMS: the *_2 clips); 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); then move speed ×boomb.move_speed. A target gone mid-flight: the bomb lands where it was.
|
||||
* [ASSUMED] no drop when the drone is dead at the release; the ATK at the release; the speed-up when the bomb lands;
|
||||
* the drone keeps flying during the cast.
|
||||
* kept its bomb on screen. Now: the drone hovers through its cast; at the release an 'atk' event of kind 'droneBomb'
|
||||
* (the client winds the Attack clip up to it and flies the bomb) and fx 'phase' {kind: 'bombed'} (render FORMS: the
|
||||
* *_2 clips, which the view starts once the Attack clip is over — the official end clip Idle_2; `e.form` →
|
||||
* 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.
|
||||
* [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).
|
||||
*/
|
||||
function kitBombd(ab) {
|
||||
const s = ab.sk.boomb;
|
||||
@@ -1489,7 +1501,12 @@ function kitBombd(ab) {
|
||||
b.fx('explode', { x, y, r: BOMB_REACH + 0.5, kind: 'bomb', tiles: 'box' });
|
||||
if (t) hurt(b, e, t, atk, 'phys');
|
||||
for (const u of alliesInTiles(b, r, c, 'box', BOMB_REACH)) if (u !== t) hurt(b, e, u, atk, 'phys', { tags: ['splash'] });
|
||||
if (ms > 0 && e.alive) b.addBuff(e, { key: 'ab:bombRun', mods: { moveMul: ms }, persist: true }); // 移速最终提升至200%
|
||||
};
|
||||
// the end of the cast (bomb_s): 移速最终提升至200%; it flies on
|
||||
const finish = (b, e) => {
|
||||
if (!e.alive) return;
|
||||
e.pauseUntil = b.time;
|
||||
if (ms > 0) b.addBuff(e, { key: 'ab:bombRun', mods: { moveMul: ms }, persist: true });
|
||||
};
|
||||
return [
|
||||
{ spawn(b, e) { e.profile.noAttack = true; } },
|
||||
@@ -1498,14 +1515,19 @@ function kitBombd(ab) {
|
||||
if (!t) return;
|
||||
a.cd = Infinity; a.left = Infinity; // 仅能触发一次
|
||||
e.skillAnimUntil = -1; // the cast is drawn through its 'atk' event: the client winds the Attack clip up to it
|
||||
// hovers through the cast, until `finish` (bounded: the bomb lands within its projectile's maxAge, 10 s)
|
||||
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, () => {
|
||||
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
|
||||
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) });
|
||||
onHit: (c) => { land(b, e, atk, c.target, c.x, c.y); landed = true; done(); } });
|
||||
b.after(BOMBD_POST_DELAY, () => { waited = true; done(); }, { owner: e });
|
||||
}, { owner: e });
|
||||
}, { cond: (b, e) => targetsNear(b, e, reach(e)).length > 0 }),
|
||||
];
|
||||
|
||||
@@ -719,12 +719,13 @@ test(`${nm('enemy_1042_frostd')}: operators within ${tb('enemy_1042_frostd', 'de
|
||||
|
||||
test(`${nm('enemy_1040_bombd')}: no normal attack; ONE bomb on the target + its 8 tiles, then move speed ×${skb('enemy_1040_bombd', 'boomb').bb.move_speed}`, () => {
|
||||
// PRTS 暴鸰: "不进行普通攻击" · 投弹 "对目标及其周围八格的我方单位造成100%物理伤害 … 技能结束后移速最终提升至200% ※此技能仅能触发一次"
|
||||
// (the bomb leaves on the Attack clip's OnAttack and flies 1 tile at 5 tiles/s — feedback D4, test/sim/feedback1d-bombd)
|
||||
// (the bomb leaves on the Attack clip's OnAttack and flies 1 tile at 5 tiles/s; the speed-up when the cast ends —
|
||||
// feedback D4, test/sim/feedback1d-bombd)
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 5 }, { chessId: 't_wall2', row: 10, col: 6 }, { chessId: 't_wall3', row: 12, col: 6 }] });
|
||||
h.step();
|
||||
const e = put(h, 'enemy_1040_bombd', [10, 7], { route: 2 });
|
||||
const s = skb('enemy_1040_bombd', 'boomb');
|
||||
h.run(s.initCooldown + enemiesMod.BOMBD_RELEASE + 1 / 5 + 0.1);
|
||||
h.run(s.initCooldown + enemiesMod.BOMBD_RELEASE + enemiesMod.BOMBD_POST_DELAY + 0.1);
|
||||
const [w1, w2, w3] = ['t_wall', 't_wall2', 't_wall3'].map((id) => h.unit(id));
|
||||
approx(w2.stats.taken, e.s.atk, 1e-6, 'the target (latest deployed in range): 100 % ATK');
|
||||
approx(w1.stats.taken, e.s.atk, 1e-6, 'a tile next to it: splash 100 %');
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// 暴鸰's skeleton (enemy_1040_bombd) carries its bomb (slot Bottle) in Idle / Move_Loop / Die and has bomb-less twins
|
||||
// (Idle_2 / Move_Begin_2 / Move_Loop_2 / Move_End_2 / Die_2: Bottle null) — the official battle prefab switches to them
|
||||
// after the drop (mode S1, buff bomb_s, replaceAnimPairs Move→Move_2, Idle→Idle_2, Die→Die_2). The view kept the
|
||||
// bottle under the drone for good and no bomb ever left it. Now: the sim's fx 'phase' { kind: 'bombed' } switches the
|
||||
// view to those clips (render/units.js FORMS) with no fx of its own, and the drop's 'atk' kind 'droneBomb' flies a
|
||||
// falling bomb (render/style.js PROJ.droneBomb, the sim's PROJECTILE_SPEEDS.droneBomb) from the drone to the target.
|
||||
// bottle under the drone for good and no bomb ever left it. Now: the drop's 'atk' kind 'droneBomb' plays the Attack
|
||||
// clip once (its OnAttack on the event) and flies a falling bomb (render/style.js PROJ.droneBomb, the sim's
|
||||
// PROJECTILE_SPEEDS.droneBomb) from the drone to the target; the sim's fx 'phase' { kind: 'bombed' } on the same frame
|
||||
// switches the view to those clips (render/units.js FORMS) with no fx of its own — they start once the Attack clip is
|
||||
// over (the official cast's end clip Idle_2), never cutting the drop short.
|
||||
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -67,7 +69,7 @@ describe('D4 暴鸰: the drone lets go of its bomb on screen', () => {
|
||||
assert.equal(w.actor.current, 'Move_Loop_2');
|
||||
});
|
||||
|
||||
test('the drop plays the Attack clip once at its own speed (its OnAttack on the event), then the bomb-less flight', async () => {
|
||||
test('the drop plays the Attack clip once at its own speed (its OnAttack on the event); the cast end → Idle_2, then the bomb-less flight', async () => {
|
||||
// the client's lookupDef: the drone's data BAT 5 s (it never attacks normally; its attack rhythm used to drive the drop)
|
||||
const ctx = fakeViewCtx(fake.P, { assets: store(KEY), cam, lookupDef: () => ({ stats: { bat: 5, aspd: 100 } }) });
|
||||
const v = new UnitView(ctx, { id: 5, side: 'enemy', kind: 'enemy', defId: KEY, spine: KEY, tier: 2, x: 6, y: 10, maxHp: 4000, motion: 'FLY' });
|
||||
@@ -81,10 +83,15 @@ describe('D4 暴鸰: the drone lets go of its bomb on screen', () => {
|
||||
frames(12); // 0.2 s: the event is rendered
|
||||
v.onAttack({ x: 5, y: 10 }, 1.2, 'droneBomb');
|
||||
v.setForm('bombed');
|
||||
v.sync(sample(ANIM.MOVE), 1.2);
|
||||
assert.equal(v.actor.current, 'Attack', 'the release frame');
|
||||
frames(50); // the rest of the 1 s clip at speed 1 (0.733 s) and a little
|
||||
assert.equal(v.actor.current, 'Move_Loop_2', 'back to flying, without the bomb (not a 5 s attack rhythm from its BAT)');
|
||||
v.sync(sample(ANIM.IDLE), 1.2); // the drone hovers through its cast
|
||||
assert.equal(v.actor.current, 'Attack', 'the release frame: the mode change does not cut the drop short');
|
||||
frames(40); // 0.667 s (BOMBD_POST_DELAY): the cast ends, 0.067 s of the clip left
|
||||
assert.equal(v.actor.current, 'Attack');
|
||||
frames(6);
|
||||
assert.equal(v.actor.current, 'Idle_2', 'the cast\'s end clip (official _endAnimKey Idle_2), not a 5 s attack rhythm from its BAT');
|
||||
v.sync(sample(ANIM.MOVE), 2.2);
|
||||
frames(30);
|
||||
assert.equal(v.actor.current, 'Move_Loop_2', 'flying on without the bomb');
|
||||
assert.equal(v.atkInterval, 5, 'a one-off drop leaves the attack rhythm alone');
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
// for the rest of its life: nothing ever left the drone. Official (PRTS 暴鸰; the client's battle prefab
|
||||
// enemy_1040_bombd + projectile_bombd): the cast plays the Attack clip, the bomb leaves on its OnAttack event (0.267 s,
|
||||
// `_waitForAttackEvent`), flies to the target as a projectile (`_speed` 5, homing, `_ignoreCamouflage`) and explodes
|
||||
// there on the target and the 8 tiles around it; the drone switches to its bomb-less mode (S1 `bomb_s`: the *_2
|
||||
// clips) and flies on at ×2. Now (content/enemies.js kitBombd): 'atk' kind 'droneBomb' + fx 'phase' { kind: 'bombed' }
|
||||
// at the release, damage on arrival, the speed-up after the blast.
|
||||
// there on the target and the 8 tiles around it; the cast ends once the bomb has landed, at least 0.667 s after the
|
||||
// release (`_fireAttackFinishWhenProjectileInvalid`, `_minPostDelayWhenProjectileInvalid`), with the bomb-less Idle_2 as
|
||||
// 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.
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -17,6 +20,7 @@ import { PROJECTILE_SPEEDS, TICK } from '../../server/sim/constants.js';
|
||||
|
||||
const KEY = 'enemy_1040_bombd';
|
||||
const RELEASE = enemiesMod.BOMBD_RELEASE;
|
||||
const POST = enemiesMod.BOMBD_POST_DELAY;
|
||||
const ability = (e) => e.mem.ab.list.find((a) => a && a.fire);
|
||||
|
||||
/** A real round: solo 绝境 match, real stage and wave, seven real operators, the 悬赏·飞行II bounty (one 暴鸰). */
|
||||
@@ -40,10 +44,10 @@ function realRound(seed = 3, round = 4) {
|
||||
}
|
||||
|
||||
describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
|
||||
test('real wave: trigger → release on the OnAttack frame (atk droneBomb + bomb-less mode) → damage on arrival → ×2 speed', () => {
|
||||
test('real wave: trigger (hover) → release on the OnAttack frame (atk droneBomb, bomb-less mode) → damage on arrival → cast end: ×2 speed', () => {
|
||||
const { m, b } = realRound();
|
||||
const log = [];
|
||||
let drone = null, castAt = null, hitAt = null, firstHit = null;
|
||||
let drone = null, castAt = null, hitAt = null, firstHit = null, castPos = null, endPos = null, runAt = null, prev = null;
|
||||
const hits = [];
|
||||
b.on('damaged', (c) => {
|
||||
if (c.source !== drone || !drone) return;
|
||||
@@ -51,15 +55,18 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
|
||||
if (hitAt == null) { hitAt = b.time; firstHit = c.target; }
|
||||
});
|
||||
while (!b.finished && b.time < 200) {
|
||||
prev = drone ? { x: drone.x, y: drone.y } : null; // where it is when the next tick starts
|
||||
b.step();
|
||||
if (!drone) drone = b.enemies.find((x) => x.defId === KEY) || null;
|
||||
if (drone && castAt == null && (ability(drone)?.casts ?? 0) > 0) {
|
||||
castAt = b.time;
|
||||
castPos = { x: drone.x, y: drone.y };
|
||||
assert.equal(hits.length, 0, 'nothing is hit in the tick of the trigger');
|
||||
assert.ok(!drone.findBuff('ab:bombRun'), 'no speed-up before the bomb went off');
|
||||
}
|
||||
for (const ev of b.drainEvents()) if (drone && ((ev[0] === 'atk' && ev[1] === drone.id) || (ev[0] === 'fx' && ev[4]?.id === drone.id))) log.push([b.time, ev]);
|
||||
if (hitAt != null && b.time > hitAt + 1) break;
|
||||
if (castAt != null && runAt == null && drone.findBuff('ab:bombRun')) { runAt = b.time; endPos = prev; }
|
||||
if (runAt != null && b.time > runAt + 1) break;
|
||||
if (castAt != null && b.time > castAt + 5) break;
|
||||
}
|
||||
assert.ok(drone, 'the drone spawned');
|
||||
assert.ok(castAt != null, 'it triggered its bomb near the operators');
|
||||
@@ -69,7 +76,7 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
|
||||
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})`);
|
||||
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');
|
||||
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');
|
||||
assert.ok(hitAt != null, 'the bomb lands');
|
||||
assert.equal(firstHit.id, relEv[2], 'on the operator it was dropped on');
|
||||
@@ -80,8 +87,13 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
|
||||
for (const x of hits.filter((y) => y.splash)) {
|
||||
assert.ok(Math.max(Math.abs(x.target.tileR - firstHit.tileR), Math.abs(x.target.tileC - firstHit.tileC)) <= 1, 'splash only on the 8 tiles around the target');
|
||||
}
|
||||
assert.ok(drone.alive, 'the drone outlives its cast in this round');
|
||||
const run = drone.findBuff('ab:bombRun');
|
||||
if (drone.alive) assert.equal(run?.mods?.moveMul, DATA.enemies[KEY].skills[0].bb.move_speed, '移速最终提升至200% after the blast');
|
||||
assert.equal(run?.mods?.moveMul, DATA.enemies[KEY].skills[0].bb.move_speed, '技能结束后移速最终提升至200%');
|
||||
const castEnd = Math.max(hitAt, relT + POST);
|
||||
assert.ok(Math.abs(runAt - castEnd) <= TICK + 1e-9, `when the cast ends (${(runAt - castAt).toFixed(3)} s after the trigger; landed ${(hitAt - castAt).toFixed(3)})`);
|
||||
assert.ok(Math.hypot(endPos.x - castPos.x, endPos.y - castPos.y) < 1e-9, 'hovering in place through the cast (its Attack clip)');
|
||||
assert.ok(Math.hypot(drone.x - endPos.x, drone.y - endPos.y) > 0.5, 'then flies on');
|
||||
assert.equal(drone.stats.attacks, 0, 'no normal attack');
|
||||
m.dispose();
|
||||
});
|
||||
@@ -122,6 +134,7 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
|
||||
h.b.kill(e, null);
|
||||
h.run(1.5);
|
||||
assert.ok(Math.abs(h.unit('t_a').stats.taken - e.s.atk) < 1e-6, 'hit for the ATK it had at the release');
|
||||
assert.ok(!e.findBuff('ab:bombRun'), 'a dead drone gets no speed-up');
|
||||
});
|
||||
|
||||
test('the drone killed before the release: no bomb, no mode change', () => {
|
||||
@@ -136,3 +149,19 @@ describe('D4 暴鸰: the bomb leaves the drone and lands', () => {
|
||||
assert.ok(!h.events.some((ev) => ev[0] === 'fx' && ev[1] === 'phase' && ev[4]?.id === e.id), 'no bomb-less mode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UnitInfo.form: every mode the renderer draws is recorded (a field opened later)', () => {
|
||||
test('掠海漂移体 stunned → 爬行模式: UnitInfo.form \'crawl\' (render/units.js FORMS starts a later view on the *_02 clips)', () => {
|
||||
const h = makeBattle({ content: 'generic', extraContent: [enemiesMod], seed: 7, autoFinish: false, timeLimit: 60, captureNoisy: true,
|
||||
defs: { chess: { t_a: chessRec({ id: 't_a', profession: 'TANK', stats: { atk: 0, maxHp: 1e7, blockCnt: 3 }, rangeGrid: [[0, 0]], skill: null }) } },
|
||||
kits: { t_a: () => ({ trait: { noAttack: true } }) }, units: [{ chessId: 't_a', row: 10, col: 3 }] });
|
||||
h.step();
|
||||
const e = h.spawn('enemy_2025_syufo', { pos: [10, 6], routeIndex: 0, mods: { speedMul: 0, atkMul: 0 } });
|
||||
const form = () => h.b.fieldMeta().units.find((u) => u.id === e.id)?.form;
|
||||
assert.equal(form(), undefined, 'hovering');
|
||||
assert.ok(h.b.applyStatus(e, 'stun', { duration: 0.2, source: null }));
|
||||
h.step();
|
||||
assert.ok(h.events.some((ev) => ev[0] === 'fx' && ev[1] === 'phase' && ev[4]?.kind === 'crawl'), 'it dropped');
|
||||
assert.equal(form(), 'crawl');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user