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.
This commit is contained in:
sganggs committed 2026-10-06 01:25:29 +08:00
1 parent fef4ea3d4b
commit f44e524afd
8 files changed
+2222 -104

No files matched your search

+311
View File
@@ -0,0 +1,311 @@
// 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);
}
});