feedback5 K1: Misery's stand-in kit (char_615_acspec, for 缄默德克萨斯 / 山 / 新约能天使 / 锏) — the three ON_DEPLOY passives at every deployment (S1 dodge / double-hit chance, S2 ATK / ASPD for 10 s, S3 burst then drag and 停顿 on unblocked ground enemies, on a redeploy), 二象命末 double hits, 四维分离, EXE-X
This commit is contained in:
1 parent
199d3d73bf
commit
cb05592a06
3 files changed
+325
-1
No files matched your search
@@ -64,7 +64,7 @@ export const KIT_FILES = Object.freeze([
|
||||
* A stand-in without a file fights with the generic kit (content/generic.js) plus its unconditional stat talents
|
||||
* (genericTalents) — the eight 预备干员 need no file.
|
||||
*/
|
||||
export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sharp2.js']);
|
||||
export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sharp2.js', 'standin-acspec.js']);
|
||||
|
||||
async function loadKitFile(file) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// server/sim/content/kits/ops/standin-acspec.js — Misery (char_615_acspec, 卫戍协议 6★ 特种·处决者) 补位 stand-in kit.
|
||||
// Stands in for 缄默德克萨斯 (tier 4, S2), 山 (tier 5, S3 + EXE-X) and 新约能天使 / 锏 (tier 6, S3 + EXE-X):
|
||||
// data/backups.json `units.char_615_acspec.standsIn`. Kit contract: ../README.md "Stand-in kits".
|
||||
// Numbers: the blackboards of data/backups.json (skill_table / character_table / battle_equip_table); what the data
|
||||
// does not carry: PRTS Misery(卫戍协议), cited where used.
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff } from '../shared/tier1.js';
|
||||
import { lonely } from '../shared/tier4.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
|
||||
const S1 = 'skchr_acspec_1';
|
||||
const S2 = 'skchr_acspec_2';
|
||||
const S3 = 'skchr_acspec_3';
|
||||
const S1_KEY = 'acspec:s1';
|
||||
const PULL_TAG = 'acspec:pull';
|
||||
/**
|
||||
* 四维分离's "周围四格": her tile and the four tiles beside it [ASSUMED: her own tile counts — an enemy she blocks stands
|
||||
* on it, and the talent is about her being alone with one enemy].
|
||||
*/
|
||||
const AROUND4 = Object.freeze([[0, 0], [-1, 0], [1, 0], [0, -1], [0, 1]]);
|
||||
/** 空间的归依's area when the record has none: range x-4, her tile and the eight around it. */
|
||||
const RING_X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
|
||||
|
||||
/** Living enemies whose body is on her tile or the four beside it, targetable or not (flyers too). */
|
||||
function enemiesAround4(battle, unit) {
|
||||
const keys = new Set(absoluteRangeKeys(AROUND4, unit.tileR, unit.tileC, unit.dir, 0));
|
||||
let n = 0;
|
||||
for (const e of battle.enemies) if (e.alive && e.deployed && !e.hidden && bodyInKeys(e, keys)) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 空间的归依 at a deployment: atk_scale × ATK physical damage to every ground enemy around her (技能范围 x-4), then each
|
||||
* enemy of that area nobody blocks is dragged to her front with 力度 `force` (0 = 小力: Battle.pullToFront, 力度 − 重量)
|
||||
* and slowed (停顿) for `sluggish` s. PRTS 备注: "※不可对空" and "施加拖动与停顿效果的实际方式为:对目标进行一次0%攻击力
|
||||
* 的近战攻击,此次附带了拖拽与停顿效果" — so the drag and the 停顿 ride a 0-damage physical hit, and an enemy that dodges it
|
||||
* (or is out of reach of a hit: invulnerable, asleep) keeps its place. [ASSUMED] that hit is not a normal attack (no
|
||||
* isAttack procs); the drag area is the damage area (the text names no other).
|
||||
*/
|
||||
function spaceBurst(battle, unit, rec) {
|
||||
const sb = rec?.bb ?? {};
|
||||
const grid = Array.isArray(rec?.rangeGrid) && rec.rangeGrid.length ? rec.rangeGrid : RING_X4;
|
||||
const keys = absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0);
|
||||
const around = battle.enemiesInKeys(keys, unit, { canHitFly: false, groundOnly: true });
|
||||
for (const e of around) battle.dealDamage(unit, e, { amount: unit.s.atk * num(sb.atk_scale, 0), type: 'phys', isSkill: true, tags: ['skill', 'acspec:burst'] });
|
||||
const landed = new Set();
|
||||
const h = battle.on('damaged', (c) => { if (c.source === unit && c.dmg?.tags?.includes(PULL_TAG)) landed.add(c.target); }, { owner: unit, priority: 1000 });
|
||||
try {
|
||||
for (const e of around) if (e.alive && !e.blockedBy) battle.dealDamage(unit, e, { amount: 0, type: 'phys', isSkill: true, tags: ['skill', PULL_TAG] });
|
||||
} finally {
|
||||
battle.off(h);
|
||||
}
|
||||
for (const e of around) {
|
||||
if (!landed.has(e) || !e.alive) continue;
|
||||
battle.pullToFront(e, unit, num(sb.force, 0));
|
||||
if (e.alive) battle.applyStatus(e, 'sluggish', { duration: num(sb.sluggish, 2.5), source: unit });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
// Misery — trait 再部署时间大幅度减少: the data's respawnTime (18 s). Talent 1 二象命末: each attack is a double hit with
|
||||
// attack@prob (PRTS 备注: it is her normal attack's basic logic; EXE-X levels 2 / 3 make it +atk ATK and a higher
|
||||
// chance). Talent 2 四维分离: ATK +atk while exactly `cnt` enemy stands on her tile or the four beside it.
|
||||
// All three skills are 被动, ON_DEPLOY — they start at every deployment (the battle-start one included, when no enemy
|
||||
// is on the field yet: S3 then hits nothing, as in the official mode; it matters on a redeploy):
|
||||
// S1 物理的服从: for the skill's duration (10 s) the talent-1 chance is attack@prob and physical dodge prob.
|
||||
// S2 战争的恭顺: for the skill's duration (10 s) ATK +atk, ASPD +attack_speed.
|
||||
// S3 空间的归依: spaceBurst above (ground only), right after each deployment (install).
|
||||
// Module EXE-X (trait atk): ATK +atk while no ally stands on the four tiles beside her (PRTS: unlike the usual EXE-X;
|
||||
// shared/tier4.js lonely, as 缄默德克萨斯's module of the same text).
|
||||
// [ASSUMED] PRTS's 存疑 note on talent 1 — a 1 s trigger cooldown that would make her wait 1 s after each attack — is not
|
||||
// modelled: her attack interval is the data's (0.93 s base).
|
||||
// Ground only, ground-targetable (data canHitFly false, no flag).
|
||||
char_615_acspec: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const moduleAtk = num(traitBb(chess).atk, 0);
|
||||
const r1 = skillRec(chess, S1), r2 = skillRec(chess, S2), r3 = skillRec(chess, S3);
|
||||
const b1 = r1?.bb ?? {}, b2 = r2?.bb ?? {};
|
||||
const talentProb = num(t0['attack@prob'], 0), s1Prob = num(b1['attack@prob'], talentProb);
|
||||
const cnt = Math.max(1, Math.floor(num(t1.cnt, 1)));
|
||||
return {
|
||||
skills: {
|
||||
[S1]: { kind: 'passive', onStart({ battle, unit }) { battle.addBuff(unit, { key: S1_KEY, duration: num(r1?.duration, 10), mods: { dodgePhys: num(b1.prob) }, tags: ['skill'] }); } },
|
||||
[S2]: { kind: 'passive', onStart({ battle, unit }) { battle.addBuff(unit, { key: 'acspec:s2', duration: num(r2?.duration, 10), mods: { atkPct: num(b2.atk), aspd: num(b2.attack_speed) }, tags: ['skill'] }); } },
|
||||
[S3]: { kind: 'passive' },
|
||||
},
|
||||
trait: { hitsFn: (battle, unit) => (battle.rng.chance(unit.findBuff(S1_KEY) ? s1Prob : talentProb) ? 2 : 1) },
|
||||
talents: [
|
||||
{ install(battle, unit) { statBuff(battle, unit, 'acspec:t1', { atkPct: num(t0.atk) }); } },
|
||||
{ install(battle, unit) { toggleBuff(battle, unit, 'acspec:t2', () => enemiesAround4(battle, unit) === cnt, { atkPct: num(t1.atk) }); } },
|
||||
],
|
||||
install(battle, unit) {
|
||||
if (moduleAtk > 0) toggleBuff(battle, unit, 'acspec:module', () => lonely(battle, unit), { atkPct: moduleAtk });
|
||||
// 空间的归依 "部署后立即": at the `deploy` of every deployment, after the deploy-time buffs (module / talent-2
|
||||
// toggles, priority 0) are on — a passive's onStart runs earlier, inside skill.reset; a 【移动】 (move: true)
|
||||
// restarts no passive, as everywhere in the engine
|
||||
if (unit.skill?.id === S3) {
|
||||
battle.on('deploy', (c) => { if (c.unit === unit && !c.move && unit.alive && unit.deployed) spaceBurst(battle, unit, r3); }, { owner: unit, priority: -10 });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
// test/content/standin_acspec.test.js — the 补位 stand-in Misery (char_615_acspec, 6★ 特种·处决者; kit
|
||||
// server/sim/content/kits/ops/standin-acspec.js) on every chess she replaces: 缄默德克萨斯 (tier 4, S2), 山 (tier 5, S3 +
|
||||
// EXE-X), 新约能天使 / 锏 (tier 6, S3 + EXE-X), normal (E2 Lv1, skill 4) and elite (E2 Lv60, skill 7). Her three skills
|
||||
// are ON_DEPLOY passives: they start at every deployment — at the battle start no enemy is on the field yet, so S3 hits
|
||||
// nothing there and matters on a redeploy (再部署时间大幅度减少: 18 s). Numbers are read back from data/backups.json.
|
||||
// Run: node --test test/content/standin_acspec.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { unitForm } from '../../shared/standIn.js';
|
||||
import { PULL_STOP_RADIUS, TICK } from '../../server/sim/constants.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const CHAR = 'char_615_acspec';
|
||||
const UNIT = BACKUPS.units[CHAR];
|
||||
const formOf = (id) => unitForm(BACKUPS, CHAR, CHESS[id].status);
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }) };
|
||||
const ALLY = 'chess_char_1_02_a'; // 角峰, a melee blocker
|
||||
|
||||
function battle(units, o = {}) {
|
||||
return makeBattle({
|
||||
defs: { enemies: ENEMIES }, units, timeLimit: o.timeLimit ?? 400, autoFinish: false, seed: o.seed ?? 5,
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'deploy', 'death'], captureNoisy: true,
|
||||
});
|
||||
}
|
||||
const MISERY = (id, standIn = true) => ({ chessId: id, row: 10, col: 4, standIn });
|
||||
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
|
||||
const tagged = (h, u, tag) => h.hooksOf('damaged').filter((c) => c.source === u && (c.dmg?.tags || []).includes(tag));
|
||||
const talentBb = (u, i) => u.def.raw.talents.find((t) => t.index === i)?.bb ?? {};
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
|
||||
test('Misery on every chess she replaces (normal + elite): her body (再部署 18 s), the backup skill as a passive, this kit; EXE-X on the tier-5 / 6 elites only', () => {
|
||||
const ids = UNIT.standsIn.flatMap((a) => [a, a.replace(/_a$/, '_b')]);
|
||||
assert.equal(ids.length, 8);
|
||||
const withModule = [];
|
||||
for (const id of ids) {
|
||||
const c = CHESS[id], form = formOf(id);
|
||||
const h = battle([MISERY(id)]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
const sk = form.skills.find((s) => s.index === c.backup.skillIndex);
|
||||
assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [CHAR, c.charId, sk.skillId, 'passive', false, 'skills'], id);
|
||||
assert.deepEqual([sk.skillType, sk.spType], ['PASSIVE', 'ON_DEPLOY'], id);
|
||||
const mod = c.backup.uniEquipId && c.status.equipLevel > 0 ? form.modules.find((m) => m.uniEquipId === c.backup.uniEquipId) : null;
|
||||
assert.equal(!!u.def.raw.module?.active, !!mod, `${id}: module`);
|
||||
if (mod) withModule.push(`${id} L${mod.level}`);
|
||||
assert.equal(u.base.atk, form.stats.atk + (mod?.attr.atk ?? 0), `${id}: ATK`);
|
||||
assert.equal(u.base.def, form.stats.def + (mod?.attr.def ?? 0), `${id}: DEF`);
|
||||
assert.equal(u.base.respawnTime, 18, `${id}: 再部署时间大幅度减少`);
|
||||
assert.deepEqual([u.profile.attack, u.profile.canHitFly, u.s.blockCnt], ['melee', false, 1], `${id}: melee, ground only, blocks 1`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${id}: ground enemies can target her`);
|
||||
done(h);
|
||||
}
|
||||
assert.deepEqual(withModule, ['chess_char_5_17_b L1', 'chess_char_6_13_b L3', 'chess_char_6_19_b L3']);
|
||||
assert.deepEqual(ids.map((id) => CHESS[id].backup.skillIndex), [1, 1, 2, 2, 2, 2, 2, 2]);
|
||||
});
|
||||
|
||||
test('Misery 二象命末: each attack is a double hit with attack@prob (10 %; EXE-X level 3: 15 % and ATK +5 %)', () => {
|
||||
for (const [id, prob, atk] of [['chess_char_5_17_a', 0.1, 0], ['chess_char_5_17_b', 0.1, 0], ['chess_char_6_19_b', 0.15, 0.05]]) {
|
||||
const h = battle([MISERY(id)]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
const seen = [];
|
||||
assert.equal(u.profile.hitsFn({ rng: { chance: (p) => { seen.push(p); return true; } } }, u), 2);
|
||||
assert.equal(u.profile.hitsFn({ rng: { chance: (p) => { seen.push(p); return false; } } }, u), 1);
|
||||
assert.deepEqual(seen, [prob, prob], `${id}: one draw per attack at ${prob}`);
|
||||
assert.equal(u.findBuff('acspec:t1')?.mods.atkPct, atk || undefined, `${id}: talent ATK`);
|
||||
h.spawn('enemy_dummy', { pos: [10, 4] });
|
||||
h.run(300);
|
||||
const per = new Map();
|
||||
for (const c of atkHits(h, u)) per.set(c.dmg.attackId, (per.get(c.dmg.attackId) ?? 0) + 1);
|
||||
const counts = [...per.values()], doubles = counts.filter((n) => n === 2).length;
|
||||
assert.ok(counts.length > 250 && counts.every((n) => n === 1 || n === 2), `${id}: ${counts.length} attacks of one or two hits`);
|
||||
assert.ok(Math.abs(doubles / counts.length - prob) < 0.06, `${id}: ${doubles} / ${counts.length} doubles ≈ ${prob}`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('Misery 四维分离: ATK +10 % while exactly one enemy stands on her tile or the four beside it (a diagonal one does not count; an air unit does)', () => {
|
||||
const h = battle([MISERY('chess_char_5_17_a')]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
const t1 = talentBb(u, 1);
|
||||
assert.deepEqual([t1.atk, t1.cnt], [0.1, 1]);
|
||||
const on = () => !!u.findBuff('acspec:t2');
|
||||
const check = (want, msg) => {
|
||||
h.step();
|
||||
assert.equal(on(), want, msg);
|
||||
approx(u.s.atk, u.base.atk * (1 + (want ? t1.atk : 0)), `${msg}: ATK`);
|
||||
};
|
||||
check(false, 'no enemy');
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
check(true, 'one in front');
|
||||
h.spawn('enemy_dummy', { pos: [11, 5] });
|
||||
check(true, 'plus a diagonal one');
|
||||
const f = h.spawn('enemy_fly', { pos: [9, 4], route: 2 });
|
||||
check(false, 'plus an air unit above her: two');
|
||||
h.b.kill(f);
|
||||
check(true, 'the air unit gone');
|
||||
h.spawn('enemy_dummy', { pos: [10, 4] });
|
||||
check(false, 'plus one on her tile: two');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('Misery EXE-X (山 精锐): ATK +10 % while no ally stands on the four tiles beside her (diagonal allies do not count); module \'none\' and the normal chess: never', () => {
|
||||
const run = (standIn, allyAt, id = 'chess_char_5_17_b') => {
|
||||
const units = [MISERY(id, standIn)];
|
||||
if (allyAt) units.push({ chessId: ALLY, row: allyAt[0], col: allyAt[1] });
|
||||
const h = battle(units);
|
||||
h.run(0.5);
|
||||
const u = h.unit(1);
|
||||
const on = !!u.findBuff('acspec:module');
|
||||
approx(u.s.atk, u.base.atk * (1 + (on ? 0.1 : 0)), `${id} ${allyAt}: ATK`);
|
||||
done(h);
|
||||
return on;
|
||||
};
|
||||
assert.equal(run(true, null), true, 'alone');
|
||||
assert.equal(run(true, [10, 5]), false, 'an ally in front');
|
||||
assert.equal(run(true, [11, 4]), false, 'an ally below');
|
||||
assert.equal(run(true, [11, 5]), true, 'an ally on a diagonal');
|
||||
assert.equal(run({ moduleId: 'none' }, null), false, 'module none');
|
||||
assert.equal(run(true, null, 'chess_char_5_17_a'), false, 'normal chess');
|
||||
});
|
||||
|
||||
test('Misery S1 物理的服从 (no chess names it — a 自选 slot may): for 10 s from every deployment, physical dodge 45 % / 60 % and the talent-1 chance 25 %', () => {
|
||||
for (const id of ['chess_char_4_16_a', 'chess_char_4_16_b']) {
|
||||
const sk = formOf(id).skills[0], bb = sk.bb;
|
||||
const h = battle([MISERY(id, { skillIndex: 0 })]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
assert.deepEqual([u.skill.id, u.skill.kind, sk.duration], ['skchr_acspec_1', 'passive', 10]);
|
||||
const prob = () => { let p = null; u.profile.hitsFn({ rng: { chance: (x) => { p = x; return false; } } }, u); return p; };
|
||||
const on = () => { approx(u.s.dodgePhys, bb.prob, `${id}: dodge ${bb.prob}`); assert.equal(prob(), bb['attack@prob'], `${id}: chance ${bb['attack@prob']}`); };
|
||||
const off = () => { assert.equal(u.s.dodgePhys, 0, `${id}: no dodge`); assert.equal(prob(), talentBb(u, 0)['attack@prob'], `${id}: the talent's chance`); };
|
||||
on();
|
||||
h.run(9.8);
|
||||
on();
|
||||
h.run(0.3);
|
||||
off();
|
||||
h.b.retreat(u);
|
||||
assert.ok(h.b.redeploy(u));
|
||||
h.step();
|
||||
on();
|
||||
h.run(10.1);
|
||||
off();
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('Misery S2 战争的恭顺 (缄默德克萨斯): for 10 s from every deployment ATK +30 % / +45 % and ASPD +15', () => {
|
||||
for (const id of ['chess_char_4_16_a', 'chess_char_4_16_b']) {
|
||||
const sk = formOf(id).skills[1], bb = sk.bb;
|
||||
const h = battle([MISERY(id)]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
assert.deepEqual([u.skill.id, sk.duration], ['skchr_acspec_2', 10]);
|
||||
const on = () => { approx(u.s.atk, u.base.atk * (1 + bb.atk), `${id}: ATK +${bb.atk}`); assert.equal(u.s.aspd, 100 + bb.attack_speed, `${id}: ASPD`); };
|
||||
const off = () => { approx(u.s.atk, u.base.atk, `${id}: ATK back`); assert.equal(u.s.aspd, 100, `${id}: ASPD back`); };
|
||||
on();
|
||||
h.run(9.8);
|
||||
on();
|
||||
h.run(0.3);
|
||||
off();
|
||||
h.b.retreat(u);
|
||||
assert.ok(h.b.redeploy(u));
|
||||
h.step();
|
||||
on();
|
||||
h.run(10.1);
|
||||
off();
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('Misery S3 空间的归依 (山 / 锏): nothing at the battle start; knocked out, back after 18 s: 210 % / 240 % ATK to the ground enemies around her, then those nobody blocks dragged to her front with 2.5 s 停顿', () => {
|
||||
for (const id of ['chess_char_5_17_a', 'chess_char_6_19_b']) {
|
||||
const sk = formOf(id).skills[2], bb = sk.bb;
|
||||
assert.deepEqual([sk.rangeId, bb.force, bb.sluggish], ['x-4', 0, 2.5]);
|
||||
const h = battle([MISERY(id), { chessId: ALLY, row: 11, col: 4 }]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
assert.equal(tagged(h, u, 'acspec:burst').length, 0, `${id}: the battle-start deployment meets no enemy`);
|
||||
const at = (pos, key = 'enemy_dummy') => h.spawn(key, { pos, ...(key === 'enemy_fly' ? { route: 2 } : {}) });
|
||||
const diag = at([11, 5]), above = at([9, 4]), held = at([11, 4]), far = at([10, 6]), fly = at([9, 3], 'enemy_fly');
|
||||
h.step();
|
||||
assert.equal(held.blockedBy?.defId, ALLY, `${id}: one enemy held by the operator below her`);
|
||||
const start = new Map([diag, above, held, far, fly].map((e) => [e, [e.x, e.y]]));
|
||||
h.b.players[0].dp = 99;
|
||||
h.b.kill(u);
|
||||
const tk = h.b.time;
|
||||
assert.ok(h.runUntil(() => u.alive && u.deployed, 30), `${id}: redeployed`);
|
||||
assert.ok(h.b.time - tk >= 18 - 1e-9 && h.b.time - tk <= 18 + 2 * TICK, `${id}: back after 18 s (${h.b.time - tk})`);
|
||||
// the burst: three ground enemies around her (the held one included); no air unit, nothing two tiles away
|
||||
const burst = tagged(h, u, 'acspec:burst');
|
||||
assert.deepEqual(new Set(burst.map((c) => c.target)), new Set([diag, above, held]), `${id}: the enemies around her`);
|
||||
const atk = u.base.atk * (1 + (talentBb(u, 0).atk ?? 0)); // not alone (the operator below), two enemies beside her
|
||||
for (const c of burst) { approx(c.amount, atk * bb.atk_scale, `${id}: ${bb.atk_scale * 100} % ATK`); assert.equal(c.type, 'phys'); assert.ok(c.dmg.isSkill); }
|
||||
// the drag and the 停顿 ride a 0-damage hit on the ones nobody blocks
|
||||
assert.deepEqual(new Set(tagged(h, u, 'acspec:pull').map((c) => c.target)), new Set([diag, above]));
|
||||
for (const e of [diag, above]) {
|
||||
approx(Math.hypot(e.x - u.x, e.y - u.y), PULL_STOP_RADIUS, `${id}: dragged to her front (weight 0, 小力)`, 1e-3);
|
||||
assert.ok(Math.hypot(e.x - start.get(e)[0], e.y - start.get(e)[1]) > 0.2, `${id}: it moved`);
|
||||
const s = e.findBuff('sluggish');
|
||||
assert.ok(s && s.timeLeft > bb.sluggish - 0.1 && s.timeLeft <= bb.sluggish + 1e-9, `${id}: 停顿 ${bb.sluggish} s`);
|
||||
}
|
||||
for (const e of [held, far, fly]) {
|
||||
assert.deepEqual([e.x, e.y], start.get(e), `${id}: ${e.defId} stays`);
|
||||
assert.equal(e.findBuff('sluggish'), null, `${id}: ${e.defId} no 停顿`);
|
||||
}
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user