365 lines
22 KiB
JavaScript
365 lines
22 KiB
JavaScript
// 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 { deploymentOf } from '../../server/sim/content/items/battle.js';
|
||
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 +16 % (DRE-X stage 3: +24 %) 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.24 : 0.16, 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 = deploymentOf(r.u);
|
||
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);
|
||
}
|
||
});
|