Files
ark-stronghold-protocol/test/content/op_thumpy.test.js
T

356 lines
23 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_thumpy.test.js — the 自选 operator kit of 珊比 (char_4235_thumpy, 6★ 本源铁卫; kit
// server/sim/content/kits/ops/op-thumpy.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 or PRP-X
// 出发的勇气 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_thumpy.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';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const THUMPY = 'char_4235_thumpy';
const FORMS = BACKUPS.units[THUMPY].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const PRPX = 'uniequip_002_thumpy';
const S1 = 'skchr_thumpy_1', S2 = 'skchr_thumpy_2', S3 = 'skchr_thumpy_3';
const TEXAS = 'chess_char_1_08_a';
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_m1: dummy('enemy_m1', { mass: 1 }),
enemy_m4: dummy('enemy_m4', { mass: 4 }), enemy_m5: dummy('enemy_m5', { mass: 5 }),
enemy_shooter: dummy('enemy_shooter', { atk: 300, bat: 1, range: 3 }),
};
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, PRPX].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
const K = (r, c) => r * 21 + c;
const sortN = (a) => [...a].sort((x, y) => x - y);
/** A battle with 珊比 as uid 1 at (row, col) facing `dir` (RIGHT), plus `others`. */
function field({ tier = 5, elite = false, mod = null, skill = 2, row = 10, col = 5, dir = 'RIGHT', others = [], seed = 5 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'elementBurst'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: THUMPY, skillIndex: skill, uniEquipId: mod }, elite, row, col, dir }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
const skillBuff = (u) => u.findBuff(`skill:${u.id}`);
/** Keep her own SP (and so her casts) out of the way (阻回). */
const noSp = (h, u) => h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true }, duration: Infinity });
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('珊比 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, range 0-1 (her own tile), blocks 3, physical melee ground-only, no 特质', () => {
assert.equal(OPERATOR_KITS[THUMPY], KITS[THUMPY]);
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], [THUMPY, 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, 10], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [3, 'melee', false, 'phys', 1.6], `${label(f)}: 本源铁卫`);
assert.deepEqual(u.liveRangeGrid, [[0, 0]], `${label(f)}: 0-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [BACKUPS.diy.operators[THUMPY].bonds, []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth && !u.s.flags.untargetable, `${label(f)}: ground enemies target her`);
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2565 / 536 / 430, E2 Lv60 3194 / 625 / 516; PRP-X +250 / +50 → +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], [2565, 536, 3194, 625]);
assert.deepEqual([modOf(5, PRPX).attr, modOf(6, PRPX).attr], [{ maxHp: 250, atk: 50 }, { maxHp: 450, atk: 80 }]);
});
test('a 自选 pick: 珊比 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(THUMPY));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(THUMPY), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: THUMPY, skillIndex: 1, uniEquipId: PRPX } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: THUMPY, skillIndex: 1, uniEquipId: PRPX } } });
});
test('triggers: every skill is MANUAL with the data\'s DEFAULT (the owner\'s 重装 exception; rawRule TAKE_DAMAGE); S1 casts with an enemy on her tile, S2 / S3 (areas larger than her range) as soon as a ground enemy is on the glue / conveyor — the kit\'s ACTIVE_RANGE; a flyer never counts', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
for (const skill of [0, 1, 2]) {
const sk = formOf(tier, elite).skills[skill];
assert.deepEqual([sk.skillType, sk.trigger.rule, sk.trigger.rawRule], ['MANUAL', 'DEFAULT', 'TAKE_DAMAGE'], `T${tier} S${skill + 1}: data`);
const { h, u } = field({ tier, elite, skill });
assert.equal(u.skill.rule, skill === 0 ? 'DEFAULT' : 'ACTIVE_RANGE');
h.spawn('enemy_fly', { pos: [10, 6] });
if (skill === 0) h.spawn('enemy_shooter', { pos: [10, 7] });
u.skill.gainSp(999);
h.run(3);
assert.deepEqual([u.skill.activations, u.stats.attacks], [0, 0], `T${tier} S${skill + 1}: no cast for a flyer (or a shooter off her tile for S1), no attack on the flyer`);
h.spawn('enemy_dummy', { pos: skill === 0 ? [10, 5] : [10, 7] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), `T${tier} S${skill + 1}: cast with a ground enemy ${skill === 0 ? 'on her tile' : 'on the skill area'}`);
done(h);
}
}
});
test('S1 “还不走?” (30 s): ATK / DEF +28 % / +37 %; she strikes every enemy she blocks at once and pushes each one radially with 小力 (weight 0: 1.7 tiles; weight 1: 0.44)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.duration, sk.spCost, sk.bb.atk, sk.bb.def, sk.bb['attack@force']], [30, elite ? 34 : 37, elite ? 0.37 : 0.28, elite ? 0.37 : 0.28, 0], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 0 });
const right = h.spawn('enemy_dummy', { pos: [10, 5.4] }), left = h.spawn('enemy_dummy', { pos: [10, 4.6] }), upE = h.spawn('enemy_m1', { pos: [10.4, 5] });
assert.ok(h.runUntil(() => u.blocking.length === 3, 1), `T${tier}: blocks all three`);
const at = [right, left, upE].map((e) => [e.x, e.y]);
u.skill.gainSp(999);
const n0 = h.hooksOf('damaged').length;
// the cast comes as she is about to attack: that attack is the first skill attack (same tick)
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
assert.deepEqual(skillBuff(u).mods, { atkPct: sk.bb.atk, defPct: sk.bb.def }, `T${tier}: the skill buff`);
const hit = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.dmg.isAttack && c.dmg.isSkill);
assert.equal(new Set(hit.map((c) => c.dmg.attackId)).size, 1, `T${tier}: one attack`);
assert.deepEqual(sortN(hit.map((c) => c.target.id)), sortN([right.id, left.id, upE.id]), `T${tier}: every blocked enemy struck at once`);
approx(right.x - at[0][0], 1.7, `T${tier}: weight 0 pushed right 1.7`);
approx(at[1][0] - left.x, 1.7, `T${tier}: weight 0 pushed left 1.7`);
approx(upE.y - at[2][1], 0.44, `T${tier}: weight 1 pushed up 0.44`);
done(h);
}
});
test('S2 “慢慢走~” glue: her tile + 4 passable tiles in PRTS\'s order — open ground: the cross on her front tile; front blocked (facing the field edge): left, right, then spreading', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.bb.max_cnt, sk.bb.interval, sk.bb.sluggish], [5, 0.7, 0.5], `T${tier}`);
const a = field({ tier, elite, skill: 1 });
noSp(a.h, a.u);
a.u.skill.addCharge(1);
a.u.skill.activate('test');
// facing RIGHT (row 0 is the bottom row: her right hand is row − 1): own (10,5), front (10,6), its right (9,6), its
// left (11,6), its front (10,7)
assert.deepEqual(sortN(a.u.mem.thumpyGlue.keys), sortN([K(10, 5), K(10, 6), K(9, 6), K(11, 6), K(10, 7)]), `T${tier}: open ground`);
done(a.h);
// facing UP on the top lane (row 12): the front (13,5) is no ground ⇒ her left (12,4), right (12,6), then from (12,6):
// its right (12,7), then from (12,7): (12,8)
const b = field({ tier, elite, skill: 1, row: 12, col: 5, dir: 'UP' });
noSp(b.h, b.u);
b.u.skill.addCharge(1);
b.u.skill.activate('test');
assert.deepEqual(sortN(b.u.mem.thumpyGlue.keys), sortN([K(12, 5), K(12, 4), K(12, 6), K(12, 7), K(12, 8)]), `T${tier}: front blocked`);
done(b.h);
}
});
test('S2 “慢慢走~” (20 s): ATK / DEF +48 % / +57 %; from the cast every 0.7 s each ground enemy on the glue (untargetable ones too) takes 停顿 0.5 s, then 37 % / 43 % ATK physical; none off the glue, no flyer, no 隐匿 one', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.duration, sk.spCost, sk.bb.atk, sk.bb.def, sk.bb.atk_scale], [20, elite ? 27 : 30, elite ? 0.57 : 0.48, elite ? 0.57 : 0.48, elite ? 0.43 : 0.37], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 1 });
noSp(h, u);
const on = h.spawn('enemy_dummy', { pos: [11, 6] }), front = h.spawn('enemy_dummy', { pos: [10, 7] });
const off = h.spawn('enemy_dummy', { pos: [10, 8] }), fly = h.spawn('enemy_fly', { pos: [9, 6] });
const ghost = h.spawn('enemy_dummy', { pos: [11, 6] }), hidden = h.spawn('enemy_dummy', { pos: [10, 7] });
h.b.addBuff(ghost, { key: 'test:untargetable', flags: { untargetable: true } });
h.b.addBuff(hidden, { key: 'test:stealth', flags: { stealth: true } });
h.step();
u.skill.addCharge(1);
u.skill.activate('test');
assert.deepEqual(skillBuff(u).mods, { atkPct: sk.bb.atk, defPct: sk.bb.def }, `T${tier}: the skill buff`);
h.runUntil(() => !u.skill.active, 25);
const glue = (e) => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.dmg.tags?.includes('thumpy:glue') && c.type === 'phys');
for (const e of [on, front, ghost]) {
const hits = glue(e);
assert.equal(hits.length, 29, `T${tier}: ${e.tileR},${e.tileC} — a pulse at 0, 0.7 … 19.6 s`);
const sl = h.hooksOf('statusApplied').filter((c) => c.source === u && c.target === e && c.status === 'sluggish');
assert.equal(sl.length, 29, `T${tier}: 停顿 each pulse`);
assert.ok(sl.every((c) => Math.abs(c.duration - 0.5) < 1e-9), `T${tier}: 0.5 s`);
}
// the hit is atk_scale × her ATK during the skill (DEF 0); the first lands in the cast's tick
const ref = glue(on)[0];
approx(ref.amount, u.base.atk * (1 + sk.bb.atk) * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`);
for (const e of [off, fly, hidden]) assert.equal(glue(e).length, 0, `T${tier}: ${e.defId} at ${e.tileR},${e.tileC} untouched`);
assert.equal(u.mem.thumpyGlue, null, `T${tier}: the glue is gone with the skill`);
done(h);
}
});
test('S2 rider: while the glue lies, another operator\'s physical damage to an enemy on it adds 15 % of 珊比\'s ATK 侵蚀损伤 (from her); off the glue, arts, and 无来源 damage add none', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.equal(sk.bb['ep_damage_ratio[damage]'], 0.15, `T${tier}`);
const { h, u } = field({ tier, elite, skill: 1, others: [{ uid: 2, chessId: TEXAS, row: 12, col: 9 }] });
const tex = h.unit(2);
noSp(h, u);
const on = h.spawn('enemy_dummy', { pos: [11, 6] }), off = h.spawn('enemy_dummy', { pos: [11, 8] });
h.step();
u.skill.addCharge(1);
u.skill.activate('test');
const ero = (e) => e.elem.erosion;
const e0 = ero(on);
h.b.dealDamage(tex, on, { amount: 100, type: 'phys' });
approx(ero(on) - e0, 0.15 * u.s.atk, `T${tier}: +15 % of her ATK`);
const burstless = h.hooksOf('damaged').filter((c) => c.type === 'element' && c.target === on).slice(-1)[0];
assert.equal(burstless.source, u, `T${tier}: the 侵蚀损伤 is hers`);
const e1 = ero(on);
h.b.dealDamage(tex, on, { amount: 100, type: 'arts' });
h.b.dealDamage(tex, on, { amount: 100, type: 'phys', sourceless: true });
assert.equal(ero(on), e1, `T${tier}: arts / 无来源 add none`);
h.b.dealDamage(tex, off, { amount: 100, type: 'phys' });
assert.equal(ero(off), 0, `T${tier}: off the glue none`);
u.skill.extend(-999);
h.step();
const e2 = ero(on);
h.b.dealDamage(tex, on, { amount: 100, type: 'phys' });
assert.equal(ero(on), e2, `T${tier}: none once the glue is gone`);
done(h);
}
});
test('S3 “不准走!” (60 s): block +2, strikes every blocked enemy, ATK +65 % / +80 %, DEF +75 % / +105 %; the belt carries unblocked ground enemies of 重量 ≤ 4 on the 4 tiles ahead towards her at 0.8 × 20 / 30 tile/s; heavier ones, flyers and blocked ones stay', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.spCost, sk.bb.atk, sk.bb.def, sk.bb.block_cnt, sk.bb.conveyor_speed, sk.bb.mass_level, sk.bb.max_cnt, sk.bb.interval],
[60, elite ? 57 : 58, elite ? 0.8 : 0.65, elite ? 1.05 : 0.75, 2, 0.8, 4, 5, 1], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 2, col: 4 });
noSp(h, u);
const m4 = h.spawn('enemy_m4', { pos: [10, 7] }), m5 = h.spawn('enemy_m5', { pos: [10, 8] });
const fly = h.spawn('enemy_fly', { pos: [10, 6] }), past = h.spawn('enemy_dummy', { pos: [10, 9] });
h.step();
u.skill.addCharge(1);
u.skill.activate('test');
assert.deepEqual(skillBuff(u).mods, { atkPct: sk.bb.atk, defPct: sk.bb.def, blockCnt: 2 }, `T${tier}: the skill buff`);
assert.equal(u.s.blockCnt, 5, `T${tier}: block 3 + 2`);
assert.deepEqual(sortN(u.mem.thumpyBelt.carryKeys), sortN([K(10, 5), K(10, 6), K(10, 7), K(10, 8)]), `T${tier}: the belt`);
const x0 = m4.x;
h.run(1);
approx(x0 - m4.x, (0.8 * 20) / 30, `T${tier}: 0.533 tile in a second`, 1e-3);
assert.deepEqual([m5.x, fly.x, past.x], [8, 6, 9], `T${tier}: 重量 5, the flyer and the enemy past the belt stay`);
assert.ok(h.runUntil(() => m4.blockedBy === u, 6), `T${tier}: carried until she blocks it`);
const xb = m4.x;
h.run(1);
assert.equal(m4.x, xb, `T${tier}: a blocked enemy is no longer carried`);
done(h);
}
});
test('S3 belt damage: every second from the cast, each targetable ground enemy on her tile and the 4 tiles ahead and each one she blocks takes 27 % / 33 % ATK physical; an enemy whose 侵蚀 bursts there loses 30 DEF more (stacking, for good)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.bb.atk_scale, sk.bb['thumpy[ep_break_water].def']], [elite ? 0.33 : 0.27, -30], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 2, col: 4 });
noSp(h, u);
const own = h.spawn('enemy_m5', { pos: [10, 4] }), ahead = h.spawn('enemy_m5', { pos: [10, 8] });
const beyond = h.spawn('enemy_m5', { pos: [10, 9] }), fly = h.spawn('enemy_fly', { pos: [10, 7] }), side = h.spawn('enemy_m5', { pos: [11, 5] });
h.step();
u.skill.addCharge(1);
u.skill.activate('test');
h.run(5.05);
const belt = (e) => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.dmg.tags?.includes('thumpy:belt'));
for (const e of [own, ahead]) {
const hits = belt(e);
assert.equal(hits.length, 6, `T${tier}: ${e.tileR},${e.tileC} — at 0, 1 … 5 s`);
assert.equal(hits[0].type, 'phys');
approx(hits[0].amount, u.base.atk * (1 + sk.bb.atk) * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`);
}
for (const e of [beyond, fly, side]) assert.equal(belt(e).length, 0, `T${tier}: ${e.defId} at ${e.tileR},${e.tileC} spared`);
// 侵蚀 bursts: on the belt ⇒ −30 more; off it ⇒ the burst's own −120 only
for (const e of [ahead, side]) h.b.dealDamage(null, e, { type: 'element', element: 'erosion', amount: 1000 });
assert.deepEqual([ahead.findBuff('skill:thumpy:beltBreak')?.mods, side.findBuff('skill:thumpy:beltBreak')], [{ defFlat: -30 }, null], `T${tier}: the belt's extra cut`);
assert.equal(ahead.s.def, Math.max(0, 0 - 120 - 30), `T${tier}: DEF 0 floor`);
assert.equal(ahead.findBuff('skill:thumpy:beltBreak').timeLeft, Infinity, `T${tier}: for good`);
h.run(8.1); // the burst lock (8 s) ends: a second burst stacks
h.b.dealDamage(null, ahead, { type: 'element', element: 'erosion', amount: 1000 });
assert.equal(ahead.findBuff('skill:thumpy:beltBreak').stacks, 2, `T${tier}: stacking`);
done(h);
}
});
test('T1 探险理论: 元素损伤 she takes ×0.85; her physical damage adds 10 % ATK 侵蚀损伤 (not her arts), or — while the target\'s 侵蚀 burst runs — shortens that burst by 1 s instead', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, row: 9 });
noSp(h, u);
assert.deepEqual(u.def.raw.talents.find((t) => t.index === 0).bb, { ep_damage_scale: 0.85, duration_dec: 1, 'ep_damage_ratio[trigger]': 0.1 }, label(f));
const e = h.spawn('enemy_dummy', { pos: [10, 8] });
h.step();
h.b.dealDamage(e, u, { type: 'element', element: 'burn', amount: 100 });
approx(u.elem.burn, 85, `${label(f)}: 元素损伤 taken ×0.85`);
h.b.dealDamage(u, e, { amount: 100, type: 'phys' });
approx(e.elem.erosion, 0.1 * u.s.atk, `${label(f)}: 10 % ATK 侵蚀损伤 (not blocking: no module bonus)`);
const e0 = e.elem.erosion;
h.b.dealDamage(u, e, { amount: 100, type: 'arts' });
assert.equal(e.elem.erosion, e0, `${label(f)}: not on arts damage`);
h.b.dealDamage(null, e, { type: 'element', element: 'erosion', amount: 1000 });
const lock = e.findBuff('erosionBurst');
assert.ok(lock, `${label(f)}: bursting`);
const left = lock.timeLeft;
h.b.dealDamage(u, e, { amount: 100, type: 'phys' });
approx(lock.timeLeft, left - 1, `${label(f)}: the burst 1 s shorter`);
done(h);
}
});
test('T2 坚硬脚板: an enemy she has damaged whose 侵蚀 bursts gives her DEF +8 (PRP-X stage 3: DEF +12, ATK +5; ≤ 30 stacks) and 250 (400) 屏障 added to one barrier, ≤ 300 % max HP; unmarked enemies nothing; the marks go when she leaves', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, row: 9 });
noSp(h, u);
const x3 = elite && mod === PRPX && tier === 6;
const t1 = u.def.raw.talents.find((t) => t.index === 1).bb;
assert.deepEqual(t1, x3 ? { def: 12, atk: 5, shield_value: 400, max_stack_cnt: 30, scale: 3 } : { def: 8, atk: 0, shield_value: 250, max_stack_cnt: 30, scale: 3 }, label(f));
const marked = h.spawn('enemy_dummy', { pos: [10, 8] }), other = h.spawn('enemy_dummy', { pos: [11, 8] });
h.step();
h.b.dealDamage(null, other, { type: 'element', element: 'erosion', amount: 1000 });
assert.equal(u.findBuff('talent:thumpy:sole'), null, `${label(f)}: an unmarked enemy's burst`);
h.b.dealDamage(u, marked, { amount: 10, type: 'arts' });
h.b.dealDamage(null, marked, { type: 'element', element: 'erosion', amount: 1000 });
assert.deepEqual(u.findBuff('talent:thumpy:sole')?.mods, x3 ? { defFlat: 12, atkFlat: 5 } : { defFlat: 8 }, `${label(f)}: the stack`);
assert.equal(u.findBuff('talent:thumpy:barrier')?.shield, t1.shield_value, `${label(f)}: 屏障`);
h.run(8.1);
h.b.dealDamage(null, marked, { type: 'element', element: 'erosion', amount: 1000 });
assert.equal(u.findBuff('talent:thumpy:sole').stacks, 2, `${label(f)}: two stacks`);
approx(u.s.def, u.base.def + 2 * t1.def, `${label(f)}: DEF`);
assert.equal(u.findBuff('talent:thumpy:barrier').shield, 2 * t1.shield_value, `${label(f)}: the same barrier grows`);
u.findBuff('talent:thumpy:barrier').shield = 3 * u.s.maxHp - 10;
h.run(8.1);
h.b.dealDamage(null, marked, { type: 'element', element: 'erosion', amount: 1000 });
approx(u.findBuff('talent:thumpy:barrier').shield, 3 * u.s.maxHp, `${label(f)}: capped at 300 % max HP`);
// she leaves the field: the marks go; back on it, the enemy's burst gives nothing until she damages it again
h.b.retreat(u);
h.b.redeploy(u);
h.step();
h.run(8.1);
h.b.dealDamage(null, marked, { type: 'element', element: 'erosion', amount: 1000 });
assert.equal(u.findBuff('talent:thumpy:sole'), null, `${label(f)}: marks cleared when she left`);
done(h);
}
});
test('PRP-X 出发的勇气 trait: every 元素损伤 she deals ×1.15 while she blocks (stages 1 and 3); ×1 when blocking nobody or without the module', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0 });
noSp(h, u);
const x = elite && mod === PRPX;
if (x) assert.deepEqual(u.def.raw.trait.bb, { ep_damage_scale: 1.15 }, label(f));
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
h.step();
h.b.dealDamage(u, far, { amount: 100, type: 'phys' });
approx(far.elem.erosion, 0.1 * u.s.atk, `${label(f)}: blocking nobody`);
const near = h.spawn('enemy_dummy', { pos: [10, 5] });
assert.ok(h.runUntil(() => near.blockedBy === u, 1), `${label(f)}: blocking`);
const g0 = far.elem.erosion;
h.b.dealDamage(u, far, { amount: 100, type: 'phys' });
approx(far.elem.erosion - g0, 0.1 * u.s.atk * (x ? 1.15 : 1), `${label(f)}: while blocking`);
done(h);
}
});