Files
ark-stronghold-protocol/test/content/op_ascln.test.js
T
sganggs f44e524afd feedback5 O13: 自选 operator kits 假日威龙陈, 莱伊 (with her 沙地兽) and 阿斯卡纶 — every skill, talent, trait and module (stage 1 / 3) at every form
- op-chen2.js: 散射手 trait (RPR-X 160 %, RPR-Y cost −8); 节约风气 (20 % own / 20 % other snipers per PRTS 修正, RPR-X stage 3
  25 / 20 / 12 %; one roll per attack, the ammoUsed events still fire); 假日余韵 (+8 ASPD; RPR-Y stage 3 +15 % / +12 and the
  skill-time water TAG: +28 % / +20); S1 ammo 4; S2 ammo 8, 蓄力 20, slime; S3 2-6, 2 hits, 32 bullets 2 per attack, slime.
- op-ray.js: 猎手 reload of her base attack time (PRTS 分支特性信息; HUN-X +1 on an empty magazine, HUN-Y ×1.33); 巡哨伙伴 and
  the 沙地兽's kit (25 s scouting: extra range and trigger range, area targets first, ×1.15 / 1.2; leaves, comes back after its
  redeploy time paying 3 DP, withdrawn with her); 入神 layers; S1 special bullet + push + kill bonus; S2 toggle at full SP
  (4-10, bullet recovery, redeploy −20 / 30 %); S3 fill at 0.4 s, 3-8, bind, +10 SP on a kill.
- op-ascln.js: 伏击客 dodge without a second taunt −1; 死亡拘审 layers (move ×(1 − 0.18 n), n × 10 % ATK arts / s; AMB-X stage 3
  11 % / 30 s; AMB-Y stage 3 heal on a marked knock-out); 噬光残影 high-ground ASPD; S1 two strikes; S2 slow on every enemy
  (PRTS 修正) and the 1.3 spread; S3 y-10, taunt +2, 命中率 cut with heals on misses / dodges; AMB-X stacking slow.
- Tests: test/content/op_chen2.test.js, op_ray.test.js, op_ascln.test.js (9 each).
- Golden: only diy moved — diy-049 … 054 now field the three new kits, the prototypes moved to diy-055 … 058 (new ids, same
  packing); diy-001 … 048 and the roster / bonds / fields / matches / standins families unchanged.
2026-10-06 01:25:29 +08:00

312 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 +8; +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 + 8 + (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: 8, 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);
}
});