Files
ark-stronghold-protocol/test/content/op_lessng.test.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

365 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 { 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);
}
});