350 lines
22 KiB
JavaScript
350 lines
22 KiB
JavaScript
// test/content/op_mgllan.test.js — the 自选 operator kit of 麦哲伦 (char_248_mgllan, 6★ 召唤师; kit
|
|
// server/sim/content/kits/ops/op-mgllan.js, the summon deck of kits/shared/summoner.js) and of her drones 龙腾.F / .L / .A,
|
|
// fielded the production way (a DIY slot + its `diy` pick, simdata getDiy; the drone as the placed hand piece of her player)
|
|
// in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, SUM-X
|
|
// 专业无人机操作模块 or SUM-Y 教学用龙腾无人机 at stage 1 (tier 5) / 3 (tier 6). Numbers from data/backups.json; the fidelity
|
|
// checklist of kits/README.md.
|
|
// Run: node --test test/content/op_mgllan.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 { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
|
import { canTargetAlly } from '../../server/sim/targeting.js';
|
|
import { diyPool, validateDiyPicks } from '../../shared/diy.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 MG = 'char_248_mgllan';
|
|
const FORMS = BACKUPS.units[MG].forms;
|
|
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
|
const SUMX = 'uniequip_002_mgllan', SUMY = 'uniequip_003_mgllan';
|
|
const S1 = 'skchr_mgllan_1', S2 = 'skchr_mgllan_2', S3 = 'skchr_mgllan_3';
|
|
const F = 'token_10005_mgllan_drone1', L = 'token_10005_mgllan_drone2', A = 'token_10005_mgllan_drone3';
|
|
const DRONE_OF = [F, L, A];
|
|
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
|
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
|
|
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
|
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
|
/** A drone's variant of a form, with the pick's skill / module (getDiyToken). */
|
|
const tokOf = (tok, tier, elite, skill, mod) => {
|
|
let v = BACKUPS.tokens[tok].variants[`${MG}@${statusOf(tier, elite)}`];
|
|
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
|
|
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
|
|
return v;
|
|
};
|
|
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' }),
|
|
enemy_archer: dummy('enemy_archer', { range: 3, atk: 100 }),
|
|
};
|
|
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, SUMX, SUMY].map((m) => [t, true, m]))];
|
|
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
|
|
|
/** 麦哲伦 as uid 1 at (row, col) facing RIGHT; `drones` = [[tokenId, row, col, dir?]] placed pieces (uid 10 +). */
|
|
function field({ tier = 5, elite = false, mod = null, skill = 0, drones = [], row = 10, col = 4, seed = 5, dp = 99 } = {}) {
|
|
const h = makeBattle({
|
|
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
|
flags: { dpPerSec: 0, dpMax: 999, dpInit: dp }, captureNoisy: true,
|
|
hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack'],
|
|
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: MG, skillIndex: skill, uniEquipId: mod }, elite, row, col },
|
|
...drones.map(([tokenId, r, c, dir], i) => ({ uid: 10 + i, kind: 'token', tokenId, ownerUid: 1, row: r, col: c, dir: dir ?? 'RIGHT' }))],
|
|
});
|
|
h.step();
|
|
const u = h.unit(1);
|
|
return { h, u, ds: h.b.allyUnits.filter((a) => a.kind === 'token'), deck: u.mem.summonDeck };
|
|
}
|
|
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src && c.dmg?.isAttack);
|
|
function done(h) {
|
|
checkInvariants(h.b);
|
|
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
|
}
|
|
|
|
test('麦哲伦 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, ranged arts, hits air, blocks 1, ground-targetable, the data triggers (DEFAULT), 协防, no 特质', () => {
|
|
assert.equal(OPERATOR_KITS[MG], KITS[MG]);
|
|
for (const f of FORMS_ALL) {
|
|
const [tier, elite, mod] = f;
|
|
for (const skill of [0, 1, 2]) {
|
|
const { h, u } = field({ tier, elite, mod, skill });
|
|
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null;
|
|
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [MG, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
|
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
|
|
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 召唤师`);
|
|
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
|
|
assert.deepEqual([u.skill.rule, form.skills[skill].trigger.rule], ['DEFAULT', 'DEFAULT'], `${label(f)}: the data's trigger`);
|
|
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
|
|
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
|
done(h);
|
|
}
|
|
}
|
|
// the numbers of the forms (zh_CN): E2 Lv1 871 / 426 / 116, E2 Lv60 1025 / 470 / 132; SUM-X +100 / +30 → +150 / +50 HP /
|
|
// ATK, SUM-Y +25 / +25 → +40 / +40 ATK / DEF
|
|
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [871, 426, 1025, 470]);
|
|
assert.deepEqual([modOf(5, SUMX).attr, modOf(6, SUMX).attr, modOf(5, SUMY).attr, modOf(6, SUMY).attr], [{ maxHp: 100, atk: 30 }, { maxHp: 150, atk: 50 }, { atk: 25, def: 25 }, { atk: 40, def: 40 }]);
|
|
});
|
|
|
|
test('麦哲伦\'s skills in every form: the data\'s SP (time SP, spCost, initSp, charges) and her 3-1 range while each runs (none changes it)', () => {
|
|
for (const f of FORMS_ALL) {
|
|
const [tier, elite, mod] = f;
|
|
for (const skill of [0, 1, 2]) {
|
|
const sk = formOf(tier, elite).skills[skill];
|
|
const { h, u } = field({ tier, elite, mod, skill });
|
|
assert.deepEqual([u.skill.spType, u.skill.spCost, u.skill.maxCharges], ['time', sk.spCost, sk.maxChargeTime], `${label(f)} S${skill + 1}: SP`);
|
|
assert.ok(Math.abs(u.skill.sp - sk.initSp) < 0.1, `${label(f)} S${skill + 1}: initSp ${sk.initSp} (${u.skill.sp})`);
|
|
h.spawn('enemy_dummy', { pos: [10, 5] });
|
|
u.skill.gainSp(999);
|
|
assert.ok(h.runUntil(() => u.skill.activations > 0, 3), `${label(f)} S${skill + 1}: cast with an enemy in her range`);
|
|
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `${label(f)} S${skill + 1}: 3-1 while it runs`);
|
|
done(h);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('a 自选 pick: 麦哲伦 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks with either module', () => {
|
|
const data = { chess: CHESS, backups: BACKUPS };
|
|
assert.ok(KITTED_CHARS.includes(MG));
|
|
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(MG), `tier ${t}`);
|
|
for (const uniEquipId of [SUMX, SUMY, null]) {
|
|
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: MG, skillIndex: 2, uniEquipId } }, { data, kitted: KITTED_CHARS }),
|
|
{ ok: true, picks: { [SLOT[6]]: { charId: MG, skillIndex: 2, uniEquipId } } });
|
|
}
|
|
});
|
|
|
|
test('T1 支援无人机·龙腾: her skill\'s drone is her piece (the others are not hers to field); the deck — 5 held at her deployment, cap 5 (8 with SUM-Y, which adds 3 on the card), at most 3 standing (4 at SUM-Y stage 3); every drone holds 禁疗, blocks nothing, has its data and no skill', () => {
|
|
for (const f of FORMS_ALL) {
|
|
const [tier, elite, mod] = f;
|
|
for (const skill of [0, 1, 2]) {
|
|
const tok = DRONE_OF[skill];
|
|
const { h, u, ds, deck } = field({ tier, elite, mod, skill, drones: [[tok, 11, 6]] });
|
|
const d = ds[0], v = tokOf(tok, tier, elite, skill, mod);
|
|
const y = elite && mod === SUMY;
|
|
assert.ok(h.b.producesToken(u, tok) && DRONE_OF.filter((x) => x !== tok).every((x) => !h.b.producesToken(u, x)), `${label(f)} S${skill + 1}: only ${tok}`);
|
|
assert.deepEqual([deck.charge, deck.cap, deck.held, deck.maxDeployed], [5, y ? 8 : 5, y ? 8 : 5, y && tier === 6 ? 4 : 3], `${label(f)} S${skill + 1}: deck`);
|
|
assert.deepEqual([d.alive, d.ownerUnit, d.kit.skill, d.skill.noSkill || !d.kit.skill], [true, u, null, true], `${label(f)}: deployed, hers, no skill`);
|
|
assert.deepEqual([d.base.maxHp, d.base.atk, d.base.def, d.base.cost, d.base.respawnTime, d.base.bat], [v.stats.maxHp, v.stats.atk, v.stats.def, v.stats.cost, v.stats.respawnTime, v.stats.bat], `${label(f)}: drone stats`);
|
|
assert.deepEqual([d.s.blockCnt, !!d.s.flags.noHeal], [0, true], `${label(f)}: no block, 禁疗`);
|
|
assert.deepEqual(d.liveRangeGrid, v.rangeGrid, `${label(f)}: drone range`);
|
|
if (tok === F) assert.deepEqual([d.profile.noAttack, d.base.atk], [true, 0], '龙腾.F: no attack (ATK 0)');
|
|
if (tok === L) assert.deepEqual([d.profile.attack, d.profile.dmgType, d.profile.canHitFly, d.liveRangeGrid], ['melee', 'arts', false, [[0, 0]]], '龙腾.L: melee arts on its tile');
|
|
if (tok === A) assert.deepEqual([d.profile.attack, d.profile.dmgType, d.profile.canHitFly, d.profile.splashRadius], ['ranged', 'phys', true, 0.75], '龙腾.A: shells, air too, blast 0.75');
|
|
done(h);
|
|
}
|
|
}
|
|
// the SUM-Y drones: F 2 / L 4 / A 5 DP ("部署费用-1 / -3"); stage 3 F 2100 HP / 250 DEF, L ATK 515, A ATK 749, ASPD +3
|
|
assert.deepEqual([F, L, A].map((t, i) => tokOf(t, 5, true, i, SUMY).stats.cost), [2, 4, 5]);
|
|
assert.deepEqual([tokOf(F, 6, true, 0, SUMY).stats.maxHp, tokOf(F, 6, true, 0, SUMY).stats.def, tokOf(L, 6, true, 1, SUMY).stats.atk, tokOf(A, 6, true, 2, SUMY).stats.atk, tokOf(A, 6, true, 2, SUMY).stats.aspd], [2100, 250, 515, 749, 103]);
|
|
});
|
|
|
|
test('the deck: a knocked-out drone comes back on its tile its redeploy time (10 s) later, paying its cost and using one held; with none held it stays off; the drones leave with her and come back after her next deployment (+5 held)', () => {
|
|
const { h, u, ds, deck } = field({ tier: 6, elite: true, mod: SUMX, skill: 2, drones: [[A, 11, 6], [A, 9, 6]] });
|
|
const [d1, d2] = ds;
|
|
const p = h.b.players[0];
|
|
h.b.kill(d1, null);
|
|
assert.deepEqual([d1.alive, d1.removed], [false, false], 'the piece stays');
|
|
h.run(9.5);
|
|
assert.equal(d1.alive, false, 'not before 10 s');
|
|
const dp0 = p.dp;
|
|
assert.ok(h.runUntil(() => d1.alive, 1), 'back after 10 s');
|
|
assert.deepEqual([d1.tileR, d1.tileC, deck.held], [11, 6, 4], 'on its tile, one held used');
|
|
approx(dp0 - p.dp, d1.base.cost, 'its cost paid');
|
|
// none held: it stays off
|
|
deck.held = 0;
|
|
h.b.kill(d1, null);
|
|
h.run(15);
|
|
assert.equal(d1.alive, false, 'none held');
|
|
deck.held = 2;
|
|
assert.ok(h.runUntil(() => d1.alive, 1), 'back once one is held');
|
|
// she leaves: her drones go with her (no knock-down); back after her redeployment
|
|
h.b.retreat(u);
|
|
h.step();
|
|
assert.deepEqual([d1.alive, d2.alive], [false, false], 'withdrawn with her');
|
|
assert.ok(h.hooksOf('death').filter((c) => c.unit === d1 || c.unit === d2).slice(-2).every((c) => c.reason === 'retreat'), 'withdrawn, not knocked out');
|
|
h.run(20);
|
|
assert.deepEqual([d1.alive, d2.alive], [false, false], 'never while she is off the field');
|
|
deck.held = 0;
|
|
assert.ok(h.b.redeploy(u), 'she comes back');
|
|
assert.ok(h.runUntil(() => d1.alive && d2.alive, 1), 'her drones follow');
|
|
assert.equal(deck.held, 3, '+5 at her deployment, two used');
|
|
done(h);
|
|
});
|
|
|
|
test('T2 光学折射配件: a drone is 隐匿 for 22 s (28 s with SUM-X stage 3) from each deployment — no ranged enemy targets it meanwhile; SUM-X stage 3: 麦哲伦 beside a drone inside that window is 隐匿 too (not two tiles away, not at stage 1)', () => {
|
|
for (const [tier, elite, mod] of [[5, false, null], [5, true, SUMX], [6, true, SUMX], [6, true, SUMY]]) {
|
|
const x3 = elite && mod === SUMX && tier === 6;
|
|
const want = x3 ? 28 : 22;
|
|
const { h, u, ds } = field({ tier, elite, mod, skill: 1, drones: [[L, 10, 5], [L, 11, 7]] });
|
|
const [near, far] = ds;
|
|
assert.equal(tokOf(L, tier, elite, 1, mod).talents.find((t) => t.bb.hidden_duration != null).bb.hidden_duration, want, `${label([tier, elite, mod])}: data`);
|
|
const archer = h.spawn('enemy_archer', { pos: [11, 9] });
|
|
assert.ok(near.s.flags.stealth && far.s.flags.stealth, '隐匿 at deployment');
|
|
assert.ok(!canTargetAlly(archer, far, true), 'a ranged enemy cannot target it');
|
|
assert.equal(!!u.s.flags.stealth, x3, `${label([tier, elite, mod])}: her own 隐匿 beside the drone`);
|
|
h.run(want - 0.5);
|
|
assert.ok(near.s.flags.stealth, 'still 隐匿');
|
|
assert.equal(!!u.s.flags.stealth, x3, 'hers still');
|
|
h.run(1);
|
|
assert.ok(!near.s.flags.stealth && !far.s.flags.stealth, `over after ${want} s`);
|
|
assert.ok(canTargetAlly(archer, far, true), 'targetable again');
|
|
assert.ok(!u.s.flags.stealth, 'hers over with it');
|
|
// a new deployment: a new window
|
|
h.b.kill(far, null);
|
|
h.runUntil(() => far.alive, 12);
|
|
assert.ok(far.s.flags.stealth, 'again at its next deployment');
|
|
done(h);
|
|
}
|
|
// her 隐匿 needs the drone on one of her four neighbouring tiles
|
|
const { h, u } = field({ tier: 6, elite: true, mod: SUMX, skill: 1, drones: [[L, 10, 6]] });
|
|
assert.ok(!u.s.flags.stealth, 'two tiles away: no');
|
|
done(h);
|
|
});
|
|
|
|
test('S1 高效制冷模块: passive — every 3 s from her deployment 停顿 0.8 / 0.9 s on every enemy of her 3-1 and her 龙腾.F\'s x-4 (air units too), none elsewhere; running (15 s) — 束缚 1.9 / 2.2 s every 3 s from the cast instead; it ends recalling her drones', () => {
|
|
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
|
|
const sk = skillOf(tier, elite, S1);
|
|
const { h, u, ds, deck } = field({ tier, elite, mod, skill: 0, row: 10, col: 3, drones: [[F, 11, 8]] });
|
|
const d = ds[0];
|
|
assert.deepEqual([sk.bb['attack@interval'], sk.bb['attack@sluggish'], sk.bb['attack@frozen_duration'], sk.duration], [3, elite ? 0.9 : 0.8, elite ? 2.2 : 1.9, 15]);
|
|
const at = (pos, key = 'enemy_dummy') => h.spawn(key, { pos });
|
|
const her = at([10, 5]), fly = at([11, 4], 'enemy_fly'), droneE = at([12, 9]), out = at([9, 9]);
|
|
const st = (status) => h.hooksOf('statusApplied').filter((c) => c.status === status);
|
|
h.run(2.9);
|
|
assert.equal(st('sluggish').length, 0, 'nothing before 3 s');
|
|
h.run(0.2);
|
|
const first = st('sluggish');
|
|
assert.deepEqual(new Set(first.map((c) => c.target)), new Set([her, fly, droneE]), 'her range + the drone\'s x-4');
|
|
for (const c of first) approx(c.duration, sk.bb['attack@sluggish'], 'duration');
|
|
assert.equal(first.find((c) => c.target === droneE).source, d, 'the drone\'s own area');
|
|
assert.ok(!first.some((c) => c.target === out), 'outside: untouched');
|
|
h.run(3);
|
|
assert.equal(st('sluggish').length, 6, 'again 3 s later');
|
|
// the cast: 束缚 every 3 s instead
|
|
u.skill.gainSp(999);
|
|
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast on an attack');
|
|
const t0 = h.b.time, n0 = st('sluggish').length;
|
|
h.run(2.9);
|
|
assert.equal(st('bind').length, 0, 'no 束缚 before 3 s into it');
|
|
h.run(0.2);
|
|
assert.deepEqual(new Set(st('bind').map((c) => c.target)), new Set([her, fly, droneE]), '束缚 on the same areas');
|
|
for (const c of st('bind')) approx(c.duration, sk.bb['attack@frozen_duration'], '束缚 duration');
|
|
h.runUntil(() => !u.skill.active, 15);
|
|
assert.equal(st('sluggish').length, n0, 'no 停顿 while it runs');
|
|
approx(h.b.time - t0, 15, '15 s', 0.01);
|
|
// 技能结束时回收所有无人机
|
|
h.step();
|
|
assert.deepEqual([d.alive, d.removed, deck.held], [false, false, deck.cap], 'recalled (+1 held, at most the cap)');
|
|
assert.ok(h.runUntil(() => d.alive, 10.5), 'back on its tile 10 s later');
|
|
done(h);
|
|
}
|
|
});
|
|
|
|
test('S2 激光开采模块: she and her drones ASPD +80 / +100 for 15 s; a 龙腾.L strikes every enemy on its tile while it runs (one at a time otherwise); recalled at the end', () => {
|
|
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMX]]) {
|
|
const sk = skillOf(tier, elite, S2);
|
|
const { h, u, ds } = field({ tier, elite, mod, skill: 1, row: 10, col: 3, drones: [[L, 10, 6]] });
|
|
const d = ds[0];
|
|
const a = h.spawn('enemy_dummy', { pos: [10, 6] }), b = h.spawn('enemy_dummy', { pos: [10, 6] });
|
|
h.run(3);
|
|
const before = from(h, d);
|
|
assert.ok(before.length >= 2, 'it attacks');
|
|
assert.ok(new Set(before.map((c) => c.dmg.attackId)).size === before.length, 'one target per attack');
|
|
for (const c of before) assert.deepEqual([c.type, Math.abs(c.amount - Math.max(d.s.atk * 0.05, d.s.atk * (1 - c.target.s.res / 100))) < 1e-6], ['arts', true], 'arts');
|
|
u.skill.gainSp(999);
|
|
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
|
|
assert.deepEqual(d.findBuff('mgllan:s2:drone')?.mods, { aspd: sk.bb.attack_speed }, 'the drone\'s bonus');
|
|
assert.equal(u.s.aspd, 100 + sk.bb.attack_speed, 'her ASPD');
|
|
assert.equal(d.s.aspd, 100 + sk.bb.attack_speed, 'the drone\'s');
|
|
const n0 = from(h, d).length;
|
|
h.run(3);
|
|
const during = from(h, d).slice(n0);
|
|
const byAttack = new Map();
|
|
for (const c of during) byAttack.set(c.dmg.attackId, [...(byAttack.get(c.dmg.attackId) ?? []), c.target]);
|
|
assert.ok([...byAttack.values()].every((ts) => ts.length === 2 && ts.includes(a) && ts.includes(b)), '群体: both enemies of its tile each attack');
|
|
// knocked out during it: back 10 s later, still inside the 15 s — with the skill's ASPD
|
|
h.b.kill(d, null);
|
|
assert.ok(h.runUntil(() => d.alive, 10.5) && u.skill.active, 'back while it runs');
|
|
assert.equal(d.s.aspd, 100 + sk.bb.attack_speed, 'a drone deployed during it takes its ASPD');
|
|
h.runUntil(() => !u.skill.active, 15);
|
|
h.step();
|
|
assert.deepEqual([d.alive, u.s.aspd], [false, 100], 'recalled; her ASPD back');
|
|
assert.ok(h.runUntil(() => d.alive, 10.5), 'back');
|
|
assert.equal(d.findBuff('mgllan:s2:drone'), null, 'no bonus after it');
|
|
done(h);
|
|
}
|
|
});
|
|
|
|
test('S3 武装打击模块: she and her drones ATK +80 % / +100 % for 15 s; a 龙腾.A\'s blast radius 0.75 → 1.25 while it runs; recalled at the end', () => {
|
|
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
|
|
const sk = skillOf(tier, elite, S3);
|
|
const { h, u, ds } = field({ tier, elite, mod, skill: 2, row: 10, col: 3, drones: [[A, 10, 5]] });
|
|
const d = ds[0];
|
|
const main = h.spawn('enemy_dummy', { pos: [10, 7] }), side = h.spawn('enemy_dummy', { pos: [10, 8] });
|
|
const close = h.spawn('enemy_fly', { pos: [10.6, 7] });
|
|
h.spawn('enemy_dummy', { pos: [11, 4] }); // hers (outside the drone's range)
|
|
h.run(5);
|
|
const before = from(h, d);
|
|
assert.ok(before.some((c) => c.target === main) && before.some((c) => c.target === close), 'its target and a flyer 0.6 away');
|
|
assert.ok(!before.some((c) => c.target === side), 'one 1.0 away: outside 0.75');
|
|
for (const c of before.filter((x) => x.target === main)) approx(c.amount, d.s.atk, 'phys (DEF 0)');
|
|
const atk0 = u.s.atk, datk0 = d.s.atk;
|
|
u.skill.gainSp(999);
|
|
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
|
|
approx(u.s.atk, atk0 * (1 + sk.bb.atk) / 1, 'her ATK +', 1e-9);
|
|
approx(d.s.atk, datk0 * (1 + sk.bb.atk), 'the drone\'s ATK +');
|
|
assert.equal(d.profile.splashRadius, 1.25, 'blast 1.25');
|
|
const n0 = from(h, d).length;
|
|
h.run(5);
|
|
assert.ok(from(h, d).slice(n0).some((c) => c.target === side), 'now 1.0 away too');
|
|
h.runUntil(() => !u.skill.active, 15);
|
|
h.step();
|
|
assert.equal(d.alive, false, 'recalled');
|
|
assert.ok(h.runUntil(() => d.alive, 10.5), 'back');
|
|
assert.deepEqual([d.profile.splashRadius, d.s.atk], [0.75, datk0], 'blast and ATK back');
|
|
done(h);
|
|
}
|
|
});
|
|
|
|
test('the recall triggers each drone\'s own skill: a stunned or silenced drone stays (PRTS 备注 "技能无法触发的场合不会产生效果"); none when she leaves the field with the skill running (her drones go with her)', () => {
|
|
const { h, u, ds } = field({ tier: 6, elite: true, mod: SUMX, skill: 2, row: 10, col: 3, drones: [[A, 10, 5], [A, 11, 5], [A, 9, 5]] });
|
|
const [ok, stunned, silenced] = ds;
|
|
h.spawn('enemy_dummy', { pos: [10, 5] });
|
|
u.skill.gainSp(999);
|
|
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
|
|
u.skill.timeLeft = 0.2;
|
|
h.b.applyStatus(stunned, 'stun', { duration: 5, force: true });
|
|
h.b.applyStatus(silenced, 'silence', { duration: 5, force: true });
|
|
h.runUntil(() => !u.skill.active, 1);
|
|
h.step();
|
|
assert.deepEqual([ok.alive, stunned.alive, silenced.alive], [false, true, true], 'only the free one');
|
|
// her leaving with the skill on: withdrawn with her, no recall (no +1 held)
|
|
const { h: h2, u: u2, ds: ds2, deck } = field({ tier: 5, elite: false, skill: 2, row: 10, col: 3, drones: [[A, 10, 5]] });
|
|
h2.spawn('enemy_dummy', { pos: [10, 5] });
|
|
u2.skill.gainSp(999);
|
|
h2.runUntil(() => u2.skill.active, 3);
|
|
deck.held = 2;
|
|
h2.b.kill(u2, null);
|
|
h2.step();
|
|
assert.deepEqual([ds2[0].alive, deck.held], [false, 2], 'gone with her, nothing held more');
|
|
done(h);
|
|
done(h2);
|
|
});
|
|
|
|
test('modules: SUM-X\'s trait (首个召唤物部署时不消耗部署位) is a prep deploy-cap rule — the deck is the same as without it; SUM-Y\'s +3 held on the card and cap 8 hold through her redeployment (+5, at most 8)', () => {
|
|
const { h, deck } = field({ tier: 6, elite: true, mod: SUMX, skill: 0 });
|
|
assert.deepEqual([deck.held, deck.cap, deck.maxDeployed], [5, 5, 3], 'SUM-X: the base deck');
|
|
assert.equal(formOf(6, true).modules.find((m) => m.uniEquipId === SUMX).traitOverride.bb.cnt, 1, 'its trait blackboard (unused)');
|
|
done(h);
|
|
const { h: h2, u, deck: d2 } = field({ tier: 5, elite: true, mod: SUMY, skill: 0 });
|
|
assert.deepEqual([d2.held, d2.cap, d2.maxDeployed], [8, 8, 3], 'SUM-Y stage 1: 3 + 5 held, cap 8, 3 standing');
|
|
d2.held = 1;
|
|
h2.b.retreat(u);
|
|
h2.b.redeploy(u);
|
|
assert.equal(d2.held, 6, '+5 at her next deployment');
|
|
h2.b.retreat(u);
|
|
h2.b.redeploy(u);
|
|
assert.equal(d2.held, 8, 'at most 8');
|
|
done(h2);
|
|
});
|