312 lines
20 KiB
JavaScript
312 lines
20 KiB
JavaScript
// test/content/op_ascln.test.js — the 自选 operator kit of 阿斯卡纶 (char_4132_ascln, 6★ 伏击客; kit
|
||
// server/sim/content/kits/ops/op-ascln.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
|
||
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, AMB-X
|
||
// “无形,无情” or AMB-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_ascln.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 { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||
import { dreadKey, DREAD_TAG } from '../../server/sim/content/kits/ops/op-ascln.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 ASC = 'char_4132_ascln';
|
||
const FORMS = BACKUPS.units[ASC].forms;
|
||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||
const AX = 'uniequip_002_ascln', AY = 'uniequip_003_ascln';
|
||
const S1 = 'skchr_ascln_1', S2 = 'skchr_ascln_2', S3 = 'skchr_ascln_3';
|
||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||
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);
|
||
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_weak: dummy('enemy_weak', { hp: 10 }),
|
||
enemy_hitter: dummy('enemy_hitter', { atk: 200, range: 1.5, bat: 0.5 }),
|
||
enemy_walkslow: enemyRec({ key: 'enemy_walkslow', hp: 1e9, speed: 1, mass: 0 }),
|
||
};
|
||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, AX, AY].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, plus `others`. */
|
||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5 } = {}) {
|
||
const h = makeBattle({
|
||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'attack', 'heal', 'dodge'], captureNoisy: true,
|
||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: ASC, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||
});
|
||
h.step();
|
||
return { h, u: h.unit(1) };
|
||
}
|
||
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
|
||
function done(h) {
|
||
checkInvariants(h.b);
|
||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||
}
|
||
/** Keep her from casting (no SP). */
|
||
function noSkill(h, u) {
|
||
u.skill.charges = 0; u.skill.sp = 0;
|
||
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
|
||
}
|
||
|
||
test('阿斯卡纶 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, y-1, 伏击客 (melee, every enemy in range, ground only, 50 % / AMB-Y 65 % dodge, taunt level −1 — not −2), blocks 0, the data triggers; offered as a pick', () => {
|
||
assert.equal(OPERATOR_KITS[ASC], KITS[ASC]);
|
||
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], [ASC, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.res],
|
||
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 30], `${label(f)}: stats`);
|
||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.allInRange, u.profile.sub, u.base.bat],
|
||
[0, 'melee', false, 'phys', true, 'stalker', 3.5], `${label(f)}: 伏击客`);
|
||
const p = mod === AY ? 0.65 : 0.5;
|
||
assert.deepEqual([u.s.dodgePhys, u.s.dodgeArts, u.s.taunt], [p, p, -1], `${label(f)}: dodge, taunt −1`);
|
||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-1`);
|
||
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
|
||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth && !u.s.flags.untargetable, `${label(f)}: ground enemies can target her`);
|
||
done(h);
|
||
}
|
||
}
|
||
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'ACTIVE_RANGE']);
|
||
assert.deepEqual([modOf(5, AX).attr, modOf(6, AX).attr, modOf(5, AY).attr, modOf(6, AY).attr],
|
||
[{ maxHp: 180, atk: 15 }, { maxHp: 270, atk: 53 }, { atk: 55, def: 25 }, { atk: 80, def: 60 }]);
|
||
const data = { chess: CHESS, backups: BACKUPS };
|
||
assert.ok(KITTED_CHARS.includes(ASC) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(ASC)));
|
||
assert.equal(validateDiyPicks({ [SLOT[6]]: { charId: ASC, skillIndex: 2, uniEquipId: AX } }, { data, kitted: KITTED_CHARS }).ok, true);
|
||
});
|
||
|
||
test('trait 伏击客: one attack strikes every ground enemy of her y-1 at once (physical), never a flyer; her dodge lets 50 % (AMB-Y 65 %) of the enemy hits through as dodges', () => {
|
||
for (const [tier, elite, mod] of [[5, false, null], [6, true, AY]]) {
|
||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||
noSkill(h, u);
|
||
const a = h.spawn('enemy_dummy', { pos: [10, 6] }), b = h.spawn('enemy_dummy', { pos: [11, 4] }), c = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||
const fly = h.spawn('enemy_fly', { pos: [9, 5] });
|
||
assert.ok(h.runUntil(() => atkHits(h, u).length >= 3, 6));
|
||
const id = atkHits(h, u)[0].dmg.attackId;
|
||
const hit = atkHits(h, u).filter((x) => x.dmg.attackId === id);
|
||
assert.deepEqual(new Set(hit.map((x) => x.target)), new Set([a, b, c]), `T${tier}: every ground enemy of y-1 ([0,2] included)`);
|
||
for (const x of hit) assert.equal(x.type, 'phys');
|
||
assert.ok(!atkHits(h, u).some((x) => x.target === fly), 'never the flyer');
|
||
// her dodge
|
||
const hitter = h.spawn('enemy_hitter', { pos: [10, 6] });
|
||
h.b.addBuff(u, { key: 'test:inv', mods: { defFlat: 1e6 } });
|
||
h.run(200);
|
||
const dodged = h.hooksOf('dodge').filter((x) => x.target === u && x.source === hitter).length;
|
||
const landed = h.hooksOf('damaged').filter((x) => x.target === u && x.source === hitter).length;
|
||
const r = dodged / (dodged + landed);
|
||
assert.ok(dodged + landed > 200 && Math.abs(r - (mod === AY ? 0.65 : 0.5)) < 0.06, `T${tier}: dodge rate ${r}`);
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('T1 死亡拘审: every hit of her attacks adds a layer (≤ 3, the duration reset): move ×(1 − 0.18 × layers), layers × 10 % of her current ATK as arts every second (法术持续伤害: not dodgeable) for 25 s; AMB-X stage 3: 11 %, 30 s; all of it ends the moment she leaves', () => {
|
||
for (const [tier, elite, mod] of [[5, false, null], [5, true, AX], [6, true, AX], [6, true, AY]]) {
|
||
const x3 = tier === 6 && mod === AX;
|
||
const tb = (elite ? formOf(tier, true) : formOf(tier, elite)).talents.find((t) => t.index === 0).bb;
|
||
const [ratio, dur] = x3 ? [0.11, 30] : [0.1, 25];
|
||
if (!x3) assert.deepEqual(tb, { move_speed: -0.18, atk_ratio: 0.1, max_stack_cnt: 3, debuff_duration: 25, interval: 1 });
|
||
else assert.deepEqual(modOf(6, AX).talentChanges.find((t) => t.talentIndex === 0).bb, { move_speed: -0.18, atk_ratio: 0.11, max_stack_cnt: 3, debuff_duration: 30, interval: 1 });
|
||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||
noSkill(h, u);
|
||
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||
h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1, dodgeArts: 1 } });
|
||
const key = dreadKey(u);
|
||
// a dodged attack lands nothing and adds nothing
|
||
h.run(4);
|
||
assert.equal(e.findBuff(key), null, `${label([tier, elite, mod])}: dodged hits add no layer`);
|
||
h.b.removeBuff(e, 'test:dodge');
|
||
const n0 = atkHits(h, u).length;
|
||
assert.ok(h.runUntil(() => atkHits(h, u).length > n0, 5));
|
||
let b = e.findBuff(key);
|
||
assert.deepEqual([b.data.n, b.duration], [1, dur], 'one layer');
|
||
approx(b.mods.moveMul, 0.82, 'move ×0.82');
|
||
// its ticks: 10 % × layers of her current ATK, arts, not dodgeable
|
||
const ticks = () => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg.tags?.includes(DREAD_TAG));
|
||
h.run(1.05);
|
||
const t1 = ticks();
|
||
assert.ok(t1.length >= 1, 'a tick');
|
||
approx(t1[0].amount, u.s.atk * ratio * 1, `${label([tier, elite, mod])}: ${ratio * 100} % ATK × 1`);
|
||
assert.deepEqual([t1[0].type, t1[0].dmg.canDodge, t1[0].dmg.isAttack], ['arts', false, false]);
|
||
assert.ok(h.runUntil(() => e.findBuff(key)?.data.n === 3, 15), 'three layers');
|
||
b = e.findBuff(key);
|
||
approx(b.mods.moveMul, 1 - 0.18 * 3, 'move ×0.46');
|
||
h.run(5);
|
||
assert.equal(e.findBuff(key).data.n, 3, '≤ 3 layers');
|
||
const last = ticks().slice(-1)[0];
|
||
approx(last.amount, u.s.atk * ratio * 3, 'three layers tick ×3');
|
||
approx(e.findBuff(key).timeLeft, dur, 'reset on each hit', 0.15);
|
||
// no more hits: it runs out after its duration
|
||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||
const tLast = atkHits(h, u).filter((c) => c.target === e).slice(-1)[0].t;
|
||
h.run(tLast + dur - 0.3 - h.b.time);
|
||
assert.ok(e.findBuff(key), 'still there');
|
||
h.run(0.6);
|
||
assert.equal(e.findBuff(key), null, `gone ${dur} s after the last hit`);
|
||
// she leaves: every layer of hers ends at once
|
||
h.b.removeBuff(u, 'test:disarm');
|
||
h.runUntil(() => e.findBuff(key), 5);
|
||
h.b.retreat(u);
|
||
assert.equal(e.findBuff(key), null, 'ends the moment she leaves');
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('T2 噬光残影: ASPD +10; +6 more with 高台 (HIGHLAND) among the four tiles next to her — the ranged wall on her left, the forbidden rim below', () => {
|
||
for (const [row, col, high] of [[10, 5, false], [10, 3, true], [12, 6, true], [11, 5, false]]) {
|
||
const { h, u } = field({ tier: 6, elite: true, skill: 1, row, col });
|
||
approx(u.s.aspd, 100 + 10 + (high ? 6 : 0), `(${row},${col}): ASPD`);
|
||
approx(u.s.interval, 3.5 * 100 / u.s.aspd, 'interval');
|
||
done(h);
|
||
}
|
||
assert.deepEqual(FORMS['2/1/4/0'].talents.find((t) => t.index === 1).bb, { attack_speed: 10, attack_speed_add: 6, cnt: 1 });
|
||
});
|
||
|
||
test('S1 追袭 (AUTO, 2 / 3 charges, data DEFAULT): the next attack strikes every enemy of her range twice at 130 % / 170 % ATK — two 死亡拘审 layers', () => {
|
||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||
const sk = skillOf(tier, elite, S1);
|
||
assert.deepEqual([sk.spCost, sk.initSp, sk.maxChargeTime, sk.bb.atk_scale, sk.skillType], elite ? [8, 4, 3, 1.7, 'AUTO'] : [10, 2, 2, 1.3, 'AUTO'], `T${tier}`);
|
||
const { h, u } = field({ tier, elite, skill: 0 });
|
||
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.maxCharges], ['charges', 'DEFAULT', elite ? 3 : 2]);
|
||
const a = h.spawn('enemy_dummy', { pos: [10, 6] }), b = h.spawn('enemy_dummy', { pos: [11, 5] });
|
||
u.skill.gainSp(999);
|
||
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), `T${tier}: cast on her next attack`);
|
||
h.step();
|
||
const hits = atkHits(h, u).filter((c) => c.dmg.isSkill);
|
||
assert.equal(hits.length, 4, `T${tier}: 2 enemies × 2 strikes`);
|
||
for (const c of hits) approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ×${sk.bb.atk_scale}`);
|
||
assert.deepEqual([a.findBuff(dreadKey(u)).data.n, b.findBuff(dreadKey(u)).data.n], [2, 2], `T${tier}: two layers each`);
|
||
assert.equal(u.skill.charges, u.skill.maxCharges - 1, 'one charge spent');
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('S2 恩赐 (MANUAL, 35 s, data DEFAULT): ATK +50 % / +90 %; every enemy of her range — air units too (PRTS 修正) — moves ×0.7 / ×0.6; a ground enemy of her range knocked out gives one 死亡拘审 layer to the ground enemies within 1.3 of it (not flyers, not beyond)', () => {
|
||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||
const sk = skillOf(tier, elite, S2);
|
||
assert.deepEqual([sk.duration, sk.spCost, sk.initSp, sk.bb.atk, sk.bb.move_speed, sk.bb.range_radius], [35, elite ? 24 : 27, elite ? 10 : 5, elite ? 0.9 : 0.5, elite ? -0.4 : -0.3, 1.3], `T${tier}`);
|
||
const { h, u } = field({ tier, elite, skill: 1 });
|
||
assert.deepEqual([u.skill.kind, u.skill.rule], ['duration', 'DEFAULT']);
|
||
const weak = h.spawn('enemy_weak', { pos: [10, 6] });
|
||
h.b.addBuff(weak, { key: 'test:dodge', mods: { dodgePhys: 1 } });
|
||
const near = h.spawn('enemy_dummy', { pos: [11, 6] }); // 1 tile from it, in her range too
|
||
const fly = h.spawn('enemy_fly', { pos: [10, 7] }); // in her range (air), 1 tile from it
|
||
const out = h.spawn('enemy_dummy', { pos: [9, 8] }); // outside her range, √5 from it
|
||
h.b.addBuff(near, { key: 'test:dodge', mods: { dodgePhys: 1 } });
|
||
u.skill.gainSp(999);
|
||
assert.ok(h.runUntil(() => u.skill.active, 5), `T${tier}: cast`);
|
||
approx(u.skill.timeLeft, 35, '35 s', 0.01);
|
||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||
h.run(0.3);
|
||
for (const e of [weak, near, fly]) approx(e.findBuff('ascln:s2slow')?.mods.moveMul ?? 1, 1 + sk.bb.move_speed, `T${tier}: ${e.defId} slowed`);
|
||
assert.equal(out.findBuff('ascln:s2slow'), null, 'not outside her range');
|
||
// the knock-out: one layer on the ground enemy next to it; the flyer and the far one get none
|
||
h.b.removeBuff(weak, 'test:dodge');
|
||
assert.ok(h.runUntil(() => !weak.alive, 10), 'knocked out');
|
||
assert.equal(near.findBuff(dreadKey(u))?.data.n, 1, `T${tier}: a layer on the ground enemy within 1.3`);
|
||
assert.equal(fly.findBuff(dreadKey(u)), null, 'never a flyer');
|
||
assert.equal(out.findBuff(dreadKey(u)), null, 'not beyond 1.3');
|
||
u.skill.extend(-999);
|
||
h.step(2);
|
||
h.run(0.5);
|
||
assert.equal(near.findBuff('ascln:s2slow'), null, `T${tier}: the slow ends with the skill`);
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('S3 降临 (MANUAL, 45 s, data ACTIVE_RANGE on y-10): an enemy only on y-10 casts it; range y-10; ATK +20 %, interval 3.5 − 1.2 / 1.5 s, taunt level −1 → +1; ground enemies of her range miss 15 % / 30 % of their attacks (one roll per attack), and a miss on her or a dodge of hers heals 4 % / 5 % of her max HP', () => {
|
||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||
const sk = skillOf(tier, elite, S3);
|
||
assert.deepEqual([sk.duration, sk.spCost, sk.initSp, sk.bb.atk, sk.bb.base_attack_time, sk.bb.taunt_level, sk.bb['attack@hp_ratio'], sk.bb['attack@damage_hitrate_physical'], sk.rangeId],
|
||
[45, elite ? 49 : 53, elite ? 30 : 25, 0.2, elite ? -1.5 : -1.2, 2, elite ? 0.05 : 0.04, elite ? -0.3 : -0.15, 'y-10'], `T${tier}`);
|
||
const { h, u } = field({ tier, elite, skill: 2 });
|
||
assert.deepEqual([u.skill.kind, u.skill.rule], ['duration', 'ACTIVE_RANGE']);
|
||
u.skill.gainSp(999);
|
||
const far = h.spawn('enemy_dummy', { pos: [12, 5] }); // [2,0]: in y-10, not in y-1
|
||
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast for [2,0]`);
|
||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: y-10 while it runs`);
|
||
approx(u.s.atk, u.base.atk * 1.2, 'ATK +20 %');
|
||
approx(u.s.interval, (3.5 + sk.bb.base_attack_time) * 100 / u.s.aspd, 'interval');
|
||
assert.equal(u.s.taunt, 1, 'taunt level +1');
|
||
h.b.kill(far, null);
|
||
// a ground hitter in her range: count its attacks before and after the 命中率 roll
|
||
const hitter = h.spawn('enemy_hitter', { pos: [10, 6] });
|
||
h.b.addBuff(hitter, { key: 'test:dodge', mods: { dodgePhys: 1 } }); // she never kills it
|
||
const seen = new Set(), passed = new Set();
|
||
h.b.on('hit', (c) => { if (c.source === hitter && c.dmg.isAttack) seen.add(c.dmg.attackId); }, { priority: 295 });
|
||
h.b.on('hit', (c) => { if (c.source === hitter && c.dmg.isAttack && !c.dmg.cancel) passed.add(c.dmg.attackId); }, { priority: 280 });
|
||
h.b.on('fatal', (c) => { if (c.unit === u) c.prevented = true; });
|
||
h.b.addBuff(u, { key: 'test:tough', mods: { defFlat: 1e6 } });
|
||
const heals0 = h.hooksOf('heal').filter((c) => c.target === u).length;
|
||
u.skill.extend(400);
|
||
h.run(300);
|
||
const r = 1 - passed.size / seen.size;
|
||
assert.ok(seen.size > 400 && Math.abs(r - Math.abs(sk.bb['attack@damage_hitrate_physical'])) < 0.05, `T${tier}: miss rate ${r} (${seen.size} attacks)`);
|
||
const dodges = h.hooksOf('dodge').filter((c) => c.target === u && c.source === hitter).length;
|
||
const heals = h.hooksOf('heal').filter((c) => c.target === u).length - heals0;
|
||
assert.equal(heals, (seen.size - passed.size) + dodges, `T${tier}: a heal per miss on her and per dodge`);
|
||
approx(h.hooksOf('heal').filter((c) => c.target === u).slice(-1)[0].amount, u.s.maxHp * sk.bb['attack@hp_ratio'], `T${tier}: ${sk.bb['attack@hp_ratio'] * 100} % max HP`, 1e-3);
|
||
u.skill.extend(-999);
|
||
h.step(2);
|
||
assert.deepEqual([u.s.taunt, u.liveRangeGrid], [-1, formOf(tier, elite).rangeGrid], `T${tier}: back`);
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('AMB-X “无形,无情”: every enemy of her range (air too) moves ×0.8 (stages 1 and 3) — two 阿斯卡纶 with it: ×0.64 (PRTS: they stack); none without it', () => {
|
||
for (const f of FORMS_ALL) {
|
||
const [tier, elite, mod] = f;
|
||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||
noSkill(h, u);
|
||
const e = h.spawn('enemy_dummy', { pos: [10, 6] }), fly = h.spawn('enemy_fly', { pos: [9, 5] });
|
||
h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
|
||
h.run(0.6);
|
||
const x = elite && mod === AX;
|
||
if (x) assert.deepEqual(formOf(tier, true).modules.find((m) => m.uniEquipId === AX).talentChanges.find((t) => t.talentIndex === -1).bb, { move_speed: -0.2 });
|
||
for (const t of [e, fly]) approx(t.findBuff(`ascln:ambx#${u.id}`)?.mods.moveMul ?? 1, x ? 0.8 : 1, `${label(f)}: ${t.defId}`);
|
||
done(h);
|
||
}
|
||
const h = makeBattle({
|
||
defs: { enemies: ENEMIES }, timeLimit: 60, autoFinish: false, seed: 3, flags: { dpPerSec: 0 },
|
||
units: [{ uid: 1, diy: { slot: 5, charId: ASC, skillIndex: 1, uniEquipId: AX }, elite: true, row: 10, col: 5 },
|
||
{ uid: 2, diy: { slot: 6, charId: ASC, skillIndex: 1, uniEquipId: AX }, elite: true, row: 11, col: 5 }],
|
||
});
|
||
h.step();
|
||
const e = h.spawn('enemy_walkslow', { pos: [10, 6] });
|
||
h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
|
||
h.run(0.5);
|
||
approx(e.s.moveSpeed, e.base.moveSpeed * 0.8 * 0.8, 'two AMB-X slows multiply');
|
||
done(h);
|
||
});
|
||
|
||
test('AMB-Y “阿斯卡纶的眼睛”: 65 % dodge (stages 1 and 3); stage 3 adds "拥有该效果的敌人被击倒时阿斯卡纶回复10%生命" — a heal of 10 % of her max HP whoever knocks out an enemy carrying her 死亡拘审', () => {
|
||
for (const [tier, elite, mod] of [[5, true, AY], [6, true, AY], [6, true, AX], [6, false, null]]) {
|
||
const y3 = tier === 6 && mod === AY;
|
||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||
noSkill(h, u);
|
||
if (y3) assert.equal(modOf(6, AY).talentChanges.find((t) => t.talentIndex === 0).bb.hp_ratio, 0.1);
|
||
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||
assert.ok(h.runUntil(() => e.findBuff(dreadKey(u)), 6), 'marked');
|
||
u.hp = 100;
|
||
const h0 = h.hooksOf('heal').filter((c) => c.target === u).length;
|
||
h.b.kill(e, null); // knocked out by anybody
|
||
const heals = h.hooksOf('heal').filter((c) => c.target === u).slice(h0);
|
||
if (y3) {
|
||
assert.equal(heals.length, 1, 'one heal');
|
||
approx(heals[0].amount, u.s.maxHp * 0.1, '10 % of her max HP');
|
||
} else assert.equal(heals.length, 0, `${tier} ${mod}: no heal`);
|
||
// an unmarked enemy: nothing
|
||
const plain = h.spawn('enemy_dummy', { pos: [12, 8] });
|
||
h.b.kill(plain, null);
|
||
assert.equal(h.hooksOf('heal').filter((c) => c.target === u).length - h0, y3 ? 1 : 0);
|
||
done(h);
|
||
}
|
||
});
|