Files
ark-stronghold-protocol/test/content/op_lessng.test.js
T
sganggs c454b61075 feedback5 O9: 自选 operator kits 帕拉斯, 止颂 and 贝洛内 (with his 牵绊) — every skill, talent, trait and module (stage 1 / 3) at every form
- op-pallas.js (char_485_pallas, 教官): 英雄的诞生 精力充沛 on every 米诺斯 operator (one buff per ally, the
  highest active bonus — her S3's too), 女神的振奋 on every damage instance (her 1-1, through 禁疗; INS-Y stage 3 the
  米诺斯 extra), S1 two strikes, S2 range +1 / ATK / stun (data DEFAULT: build-data's ATTACK_RANGE_CHANGE misses
  "攻击范围向前延伸一格" — reported, not worked around), S3 three targets and the front-holder buff (low ground,
  operators only, passed on retreat / deploy). INS-Y's "可以额外部署在远程位" is a prep placement rule (owner's
  decision of 2026-10-04): unchanged, reported.
- op-lessng.js (char_4011_lessng, 无畏者): 苦痛专注 (hit-time cut while blocking, 无来源 / allies / himself
  included), 痛楚砺刃 (any damage aimed at him, dodged too), S2 the deploy window (2 strikes, ATK, talent ×2), S3
  status immunity, blocked-target scale, the cast under a 异常状态 (600 arts first, cleanse, then HP; 沉默 does
  not hold it back), DRE-X ×1.15 on a blocked target, DRE-Y once-per-deployment save and stage 3's 12 % DEF ignore.
- op-demetr.js (char_4037_demetr, 斗士): 【手段】 (one per enemy, stacks before the hit, 8 and 停顿 under S2, back
  to 5 on the 0.1 s update), the HP-ratio damage bonus, 街头直觉 from every deployment (a 【移动】 too), S1 two
  strikes, S2 3-13 / three targets, S3 the 【移动】s (x-2 around his located tile, buildable low-ground tiles,
  in-place move without a target, re-target after 1 s without damage or when the elite he hit falls), 牵绊
  (无敌 / untargetable / 孤立), the fatal-hit 不死 + end, the 【返回】 (SP emptied, a short 不死) and the forced
  retreat; FGT-Y ASPD above half HP.
- kits/index.js: the three files appended to OPERATOR_KIT_FILES.
- Tests: test/content/op_pallas.test.js (9), op_lessng.test.js (10), op_demetr.test.js (10).
- Golden: only diy moved — diy-049 … 054 now field the three operators (every form × module × skill), the
  prototype / reserve battles moved down to diy-054 … 058 (new ids, same packing); diy-001 … 048 and every other
  family unchanged.
2026-10-06 01:30:26 +08:00

364 lines
22 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_lessng.test.js — the 自选 operator kit of 止颂 (char_4011_lessng, 6★ 无畏者; kit
// server/sim/content/kits/ops/op-lessng.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, DRE-X
// “沉锋之束” or DRE-Y “苦修者的抗压训练” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json
// (the form of that slot status); the fidelity checklist of kits/README.md item by item.
// Run: node --test test/content/op_lessng.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { DUEL_KEY, OATH_KEY, PAIN_KEY, REBORN_KEY } from '../../server/sim/content/kits/ops/op-lessng.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 LESSNG = 'char_4011_lessng';
const FORMS = BACKUPS.units[LESSNG].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const DREX = 'uniequip_002_lessng', DREY = 'uniequip_003_lessng';
const S1 = 'skchr_lessng_1', S2 = 'skchr_lessng_2', S3 = 'skchr_lessng_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 talentOf = (u, i) => u.def.raw.talents.find((t) => t.index === i)?.bb ?? {};
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_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, DREX, DREY].map((m) => [t, true, m]))];
/** A plain ally (no kit, no damage modifiers) for friendly-damage checks. */
const PLAIN = chessRec({ id: 'test_plain_a', name: '测试', skill: null });
/** A battle with 止颂 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, chess: { test_plain_a: PLAIN } }, timeLimit: 600, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: LESSNG, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
/** 止颂 on row 9 with a walker blocked by her. */
function blocked(o = {}) {
const r = field({ row: 9, ...o });
const w = r.h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(r.h.runUntil(() => w.blockedBy === r.u, 30), 'the walker reaches her');
return { ...r, w };
}
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
const noSp = (h, u) => h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } }); // 阻回: the MANUAL skill stays off
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
test('止颂 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, blocks 1, melee ground-only, 1.5 s, 莱塔尼亚, no 特质', () => {
assert.equal(OPERATOR_KITS[LESSNG], KITS[LESSNG]);
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], [LESSNG, 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.base.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.sub, u.base.bat, u.dmgType], [1, 'melee', false, 'fearless', 1.5, 'phys'], `${label(f)}: 无畏者`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.raw.nationId], [['emptyShip'], [], 'leithanien'], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
done(h);
}
}
// E2 Lv1 2798 / 866 / 232, E2 Lv60 3384 / 984 / 262; DRE-X +230 / +17 / +17 → +400 / +30 / +30; DRE-Y +300 / +55 → +450 / +80
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], [2798, 866, 3384, 984]);
assert.deepEqual([modOf(5, DREX).attr, modOf(6, DREX).attr, modOf(5, DREY).attr, modOf(6, DREY).attr],
[{ maxHp: 230, atk: 17, def: 17 }, { maxHp: 400, atk: 30, def: 30 }, { maxHp: 300, atk: 55 }, { maxHp: 450, atk: 80 }]);
});
test('a 自选 pick: 止颂 is offered at tiers 5 and 6 and a roster with him passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(LESSNG));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(LESSNG), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: LESSNG, skillIndex: 1, uniEquipId: DREY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: LESSNG, skillIndex: 1, uniEquipId: DREY } } });
});
test('T1 苦痛专注: while he blocks, physical / arts damage from anyone but an enemy he blocks — 无来源, allies, himself included — ×0.65 (DRE-Y stage 3: ×0.6); none while not blocking, none on true damage', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y3 = elite && mod === DREY && tier === 6;
const cut = talentOf(field({ tier, elite, mod }).u, 0).damage_resistance;
assert.equal(cut, y3 ? 0.4 : 0.35, `${label(f)}: the talent of the form`);
const { h, u, w } = blocked({ tier, elite, mod, others: [{ uid: 2, chessId: 'test_plain_a', row: 11, col: 3 }] });
const ally = h.unit(2);
const other = h.spawn('enemy_dummy', { pos: [11, 8] });
const hit = (src, d) => { u.hp = u.s.maxHp; return h.b.dealDamage(src, u, d); }; // (full HP: the return value is the HP removed)
const arts = (src) => hit(src, { amount: 1000, type: 'arts' });
approx(arts(other), 1000 * (1 - cut), `${label(f)}: an enemy he does not block`);
approx(arts(w), 1000, `${label(f)}: the enemy he blocks`);
approx(arts(null), 1000 * (1 - cut), `${label(f)}: 无来源`);
approx(arts(ally), 1000 * (1 - cut), `${label(f)}: an ally`);
approx(arts(u), 1000 * (1 - cut), `${label(f)}: himself`);
approx(hit(other, { amount: 1000, type: 'true' }), 1000, `${label(f)}: true damage`);
approx(hit(other, { amount: 2000, type: 'phys' }), (2000 - u.s.def) * (1 - cut), `${label(f)}: physical`);
u.hp = u.s.maxHp;
h.b.releaseBlocked(u);
h.b.kill(w, null);
h.step();
assert.equal(u.blocking.length, 0);
approx(arts(other), 1000, `${label(f)}: not while he blocks nobody`);
done(h);
}
});
test('T2 痛楚砺刃: any damage aimed at him (a dodged one too) ⇒ ATK +12 % (DRE-X stage 3: +20 %) for 15 s, refreshed, never stacked', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod });
const t = talentOf(u, 1);
const x3 = elite && mod === DREX && tier === 6;
assert.deepEqual([t.atk, t.add_atk_duration], [x3 ? 0.2 : 0.12, 15], `${label(f)}: the talent of the form`);
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
assert.equal(u.findBuff(PAIN_KEY), null);
h.b.addBuff(u, { key: 'test:dodge', mods: { dodgePhys: 1 } });
assert.equal(h.b.dealDamage(e, u, { amount: 500, type: 'phys' }), 0, `${label(f)}: dodged`);
assert.deepEqual(u.findBuff(PAIN_KEY)?.mods, { atkPct: t.atk }, `${label(f)}: still triggered`);
approx(u.s.atk, u.base.atk * (1 + t.atk), `${label(f)}: ATK +${t.atk * 100} %`);
h.b.removeBuff(u, 'test:dodge');
h.run(10);
h.b.dealDamage(e, u, { amount: 100, type: 'arts' });
h.b.dealDamage(null, u, { amount: 100, type: 'true' });
approx(u.findBuff(PAIN_KEY).timeLeft, 15, `${label(f)}: refreshed to 15 s`, 0.01);
approx(u.s.atk, u.base.atk * (1 + t.atk), `${label(f)}: not stacked`);
h.run(15.1);
assert.equal(u.findBuff(PAIN_KEY), null, `${label(f)}: gone 15 s after the last hit`);
done(h);
}
});
test('S1 强力击·γ型 (AUTO, attack SP 3, data DEFAULT): the next attack at 205 % / 225 % ATK (× DRE-X 115 % on a blocked target)', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREX], [5, true, DREY]]) {
const sk = skillOf(tier, elite, S1);
const { h, u, w } = blocked({ tier, elite, mod, skill: 0 });
assert.deepEqual([u.skill.rule, u.skill.spType, u.skill.spCost, sk.skillType, sk.bb.atk_scale], ['DEFAULT', 'attack', 3, 'AUTO', elite ? 2.25 : 2.05], `T${tier}`);
const scale = mod === DREX ? modOf(tier, DREX).traitOverride.bb.atk_scale : 1;
const t0 = h.b.time;
assert.ok(h.runUntil(() => atkHits(h, u).some((c) => c.dmg.isSkill && c.t > t0), 20), `T${tier}: cast`);
const hits = atkHits(h, u).filter((c) => c.t > t0 && c.target === w);
const sk1 = hits.find((c) => c.dmg.isSkill);
approx(sk1.amount, u.s.atk * sk.bb.atk_scale * scale, `T${tier}: ${sk.bb.atk_scale * 100} %`);
for (const c of hits.filter((x) => !x.dmg.isSkill)) approx(c.amount, u.s.atk * scale, `T${tier}: plain attacks`);
done(h);
}
});
test('S2 虔修对决 (PASSIVE, 部署后): for 18 / 21 s from every deployment ATK +25 / 35 %, two strikes per attack and 苦痛专注 ×2; then single strikes', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREY], [6, true, DREX]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.skillType, sk.spType, sk.duration, sk.bb.atk, sk.bb.talent_scale], ['PASSIVE', 'ON_DEPLOY', elite ? 21 : 18, elite ? 0.35 : 0.25, 2], `T${tier}`);
const { h, u, w } = blocked({ tier, elite, mod, skill: 1 });
const cut = talentOf(u, 0).damage_resistance;
assert.ok(h.b.time < sk.duration, 'the walker arrived inside the window');
assert.deepEqual(u.findBuff(DUEL_KEY)?.mods, { atkPct: sk.bb.atk }, `T${tier}: ATK +${sk.bb.atk * 100} %`);
const other = h.spawn('enemy_dummy', { pos: [11, 8] });
approx(h.b.dealDamage(other, u, { amount: 1000, type: 'arts' }), 1000 * (1 - cut * 2), `T${tier}: 苦痛专注 ×2`);
const t0 = h.b.time;
h.runUntil(() => !u.findBuff(DUEL_KEY), 30);
approx(h.b.time, sk.duration, `T${tier}: the window ends ${sk.duration} s after the deployment`, 0.05);
const inWin = new Map();
for (const c of atkHits(h, u).filter((x) => x.t > t0)) inWin.set(c.dmg.attackId, (inWin.get(c.dmg.attackId) ?? 0) + 1);
assert.ok(inWin.size >= 2 && [...inWin.values()].every((n) => n === 2), `T${tier}: two strikes per attack (${[...inWin.values()]})`);
const t1 = h.b.time;
h.run(5);
const after = new Map();
for (const c of atkHits(h, u).filter((x) => x.t > t1)) after.set(c.dmg.attackId, (after.get(c.dmg.attackId) ?? 0) + 1);
assert.ok(after.size >= 2 && [...after.values()].every((n) => n === 1), `T${tier}: single strikes after`);
approx(u.s.atk, u.base.atk * (1 + (u.findBuff(PAIN_KEY) ? talentOf(u, 1).atk : 0)), `T${tier}: ATK back`);
approx(h.b.dealDamage(other, u, { amount: 1000, type: 'arts' }), 1000 * (1 - cut), `T${tier}: 苦痛专注 ×1`);
// a redeploy restarts the window
h.b.retreat(u);
h.b.redeploy(u);
h.step();
approx(u.findBuff(DUEL_KEY)?.timeLeft ?? 0, sk.duration, `T${tier}: again for ${sk.duration} s`, 0.05);
assert.equal(w.alive, true);
done(h);
}
});
test('S3 苦修破誓 (MANUAL, data DEFAULT): 20 s, max HP +65 / 80 %, a blocked target at 160 % / 180 % ATK (× DRE-X), no 异常状态 while it runs; then all back', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREX], [5, true, DREY]]) {
const sk = skillOf(tier, elite, S3);
const b = sk.bb;
assert.deepEqual([sk.duration, b.max_hp, b['lessng_s3[atk_scale].atk_scale'], b.magical_value, sk.skillType], [20, elite ? 0.8 : 0.65, elite ? 1.8 : 1.6, 600, 'MANUAL'], `T${tier}`);
const { h, u, w } = blocked({ tier, elite, mod, skill: 2 });
assert.equal(u.skill.rule, 'DEFAULT');
const unb = h.spawn('enemy_dummy', { pos: [9, 5] }); // on his tile beside the walker: he blocks only one
const hp0 = u.hp, max0 = u.s.maxHp;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
approx(u.skill.timeLeft, 20, `T${tier}: 20 s`, 0.05);
approx(u.s.maxHp, max0 * (1 + b.max_hp), `T${tier}: max HP +${b.max_hp * 100} %`);
approx(u.hp / u.s.maxHp, hp0 / max0, `T${tier}: ratio kept`);
for (const st of ['stun', 'cold', 'freeze', 'sleep', 'silence', 'fear']) assert.equal(h.b.applyStatus(u, st, { duration: 3, source: w }), false, `T${tier}: immune to ${st}`);
const scale = mod === DREX ? modOf(tier, DREX).traitOverride.bb.atk_scale : 1;
const t0 = h.b.time;
h.run(6);
const hits = atkHits(h, u).filter((c) => c.t > t0);
assert.ok(hits.length >= 3);
for (const c of hits) {
if (c.target === w) approx(c.amount, u.s.atk * b['lessng_s3[atk_scale].atk_scale'] * scale, `T${tier}: blocked ×${b['lessng_s3[atk_scale].atk_scale']}`);
else approx(c.amount, unb.blockedBy ? u.s.atk * b['lessng_s3[atk_scale].atk_scale'] * scale : u.s.atk, `T${tier}: the other target`);
}
h.runUntil(() => !u.skill.active, 20);
h.step();
assert.equal(u.findBuff(OATH_KEY), null);
approx(u.s.maxHp, max0, `T${tier}: max HP back`);
assert.equal(h.b.applyStatus(u, 'stun', { duration: 0.5, source: w }), true, `T${tier}: stuns land again`);
done(h);
}
});
test('S3 苦修破誓 cast under a 异常状态: 600 arts to himself first (苦痛专注 applies while he blocks; may knock him out), every 异常状态 cleansed, then the HP; 沉默 does not hold the cast back, 晕眩 does (no attack)', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREX]]) {
const cut = 0.35;
// cold: he still attacks, the basic strategy casts; he blocks ⇒ 600 × 0.65
{
const { h, u, w } = blocked({ tier, elite, mod, skill: 2 });
h.b.applyStatus(u, 'cold', { duration: 20, source: w });
const hp0 = u.hp, max0 = u.s.maxHp;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 5), `T${tier}: cast while cold`);
const self = h.hooksOf('damaged').filter((c) => c.source === u && c.target === u);
assert.equal(self.length, 1, `T${tier}: one self hit`);
assert.deepEqual([self[0].type, Math.round(self[0].amount)], ['arts', Math.round(600 * (1 - cut))], `T${tier}: 600 arts ×0.65`);
assert.ok(!u.buffs.some((x) => x.status === 'cold'), `T${tier}: cold cleansed`);
approx(u.hp, (hp0 - 600 * (1 - cut)) / max0 * u.s.maxHp, `T${tier}: the HP buff after the damage (ratio of the hurt HP)`);
assert.ok(u.findBuff(PAIN_KEY), `T${tier}: 痛楚砺刃 saw it`);
done(h);
}
// 沉默: the engine's basic strategy refuses a silenced cast — this skill "可以无视异常状态开启"
{
const { h, u } = field({ tier, elite, mod, skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.applyStatus(u, 'silence', { duration: 20, source: null });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 5), `T${tier}: cast while silenced`);
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u)?.reason, 'abnormal');
assert.equal(Math.round(h.hooksOf('damaged').find((c) => c.source === u && c.target === u)?.amount), 600, `T${tier}: 600 arts (he blocks nobody)`);
assert.ok(!u.s.flags.silence, `T${tier}: silence cleansed`);
done(h);
}
// 晕眩: no attack ⇒ no cast until it ends (then no abnormal state, no self damage)
{
const { h, u } = field({ tier, elite, mod, skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.applyStatus(u, 'stun', { duration: 3, source: null });
u.skill.gainSp(999);
h.run(2.5);
assert.equal(u.skill.activations, 0, `T${tier}: not while stunned`);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: once the stun ends`);
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.target === u).length, 0, `T${tier}: no self damage`);
done(h);
}
// the self damage may knock him out (可致死)
{
const { h, u } = field({ tier, elite, mod, skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.applyStatus(u, 'cold', { duration: 20, source: null });
u.hp = 300;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => !u.alive, 5), `T${tier}: knocked out by his own 600`);
assert.equal(u.findBuff(OATH_KEY), null);
done(h);
}
}
});
test('DRE-X “沉锋之束”: his attacks ×115 % on a target blocked by anyone (another operator\'s too), ×1 on an unblocked one — stages 1 and 3; none without it', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 6 }] });
noSp(h, u);
const yak = h.unit(2);
const e = h.spawn('enemy_dummy', { pos: [10, 6] }); // blocked by 角峰, on his front tile
h.step();
assert.equal(e.blockedBy, yak);
const x = elite && mod === DREX;
if (x) assert.equal(u.def.raw.trait.bb.atk_scale, 1.15, label(f));
h.run(6);
const hits = atkHits(h, u).filter((c) => c.target === e);
assert.ok(hits.length >= 3, label(f));
for (const c of hits) approx(c.amount, u.s.atk * (x ? 1.15 : 1), `${label(f)}: ×${x ? 1.15 : 1}`);
h.b.releaseBlocked(yak);
h.b.retreat(yak);
h.step();
assert.ok(!e.blockedBy);
const t0 = h.b.time;
h.run(4);
for (const c of atkHits(h, u).filter((y) => y.t > t0)) approx(c.amount, u.s.atk, `${label(f)}: unblocked ×1`);
done(h);
}
});
test('DRE-Y “苦修者的抗压训练”: the first lethal hit of a deployment ⇒ max HP ×0.4, ASPD +30, full HP (once; again after a redeploy; not while a 不死 holds him); stage 3: 12 % of DEF ignored on the enemy he blocks', () => {
for (const tier of [5, 6]) {
const { h, u } = field({ tier, elite: true, mod: DREY });
const tb = u.def.raw.trait.bb;
assert.deepEqual([tb.attack_speed, tb.max_hp, tb.hp_ratio, tb.value], [30, 0.4, 1, 0.6], `T${tier}: trait bb`);
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
const max0 = u.s.maxHp, aspd0 = u.s.aspd;
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(u.alive, `T${tier}: not knocked out`);
approx(u.s.maxHp, max0 * 0.4, `T${tier}: max HP ×0.4`);
approx(u.hp, u.s.maxHp, `T${tier}: full HP of the new max`);
assert.equal(u.s.aspd, aspd0 + 30, `T${tier}: ASPD +30`);
assert.ok(u.findBuff(REBORN_KEY));
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(!u.alive, `T${tier}: once per deployment`);
h.b.redeploy(u);
h.step();
approx(u.s.maxHp, max0, `T${tier}: a new deployment: max HP back`);
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(u.alive, `T${tier}: again after the redeploy`);
done(h);
// a running 不死 (坚固维式重锤's window) holds him instead: the module is not used
const r = field({ tier, elite: true, mod: DREY });
r.u.mem.undyingAt = `${r.u.deploySeq}:0`;
r.u.mem.undyingUntil = r.h.b.time + 5;
const hammer = r.h.b.on('fatal', (c) => { if (c.unit === r.u && !c.prevented) c.prevented = true; }, { priority: -99 });
const f = r.h.spawn('enemy_dummy', { pos: [11, 8] });
r.h.b.dealDamage(f, r.u, { amount: 1e7, type: 'true' });
assert.ok(r.u.alive && r.u.hp < 2 && !r.u.findBuff(REBORN_KEY), `T${tier}: held by the 不死, the module untouched`);
r.h.b.off(hammer);
r.u.mem.undyingUntil = 0;
r.h.b.dealDamage(f, r.u, { amount: 1e7, type: 'true' });
assert.ok(r.u.alive && r.u.findBuff(REBORN_KEY), `T${tier}: the module afterwards`);
done(r.h);
}
// stage 3's hidden part: 无视目标12%的防御力 on the enemy he blocks (none at stage 1, none on an unblocked enemy)
for (const tier of [5, 6]) {
const ENEMY_DEF = 1000;
const h = makeBattle({
defs: { enemies: { enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0, def: ENEMY_DEF }), enemy_dummy: dummy('enemy_dummy', { def: ENEMY_DEF }) } },
timeLimit: 600, autoFinish: false, seed: 5, flags: { dpPerSec: 0 }, hooks: ['damaged'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: LESSNG, skillIndex: 2, uniEquipId: DREY }, elite: true, row: 9, col: 5 }],
});
h.step();
const u = h.unit(1);
noSp(h, u);
const w = h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(h.runUntil(() => w.blockedBy === u, 30));
const t0 = h.b.time;
h.run(6);
const pen = tier === 6 ? 0.12 : 0;
const hits = atkHits(h, u).filter((c) => c.t > t0 && c.target === w);
assert.ok(hits.length >= 3);
for (const c of hits) approx(c.amount, Math.max(u.s.atk - ENEMY_DEF * (1 - pen), u.s.atk * 0.05), `T${tier}: DEF ×${1 - pen}`);
done(h);
}
});