feedback5: merge O22 — 自选 kits for 淬羽赫默 (with her 夜灯), 遥 and 酒神 (with her 本能的召唤 and 迷狂牢笼); engine (additive): enemyAttackStart hook before 麻痹, blocking noNewBlock flag, holdsUndying reads an undying buff; golden: only diy moved

This commit is contained in:
sganggs committed 2026-10-06 14:15:51 +08:00
commit c9429dc2d4
13 files changed
+2417 -116

No files matched your search

+345
View File
@@ -0,0 +1,345 @@
// test/content/op_haruka.test.js — the 自选 operator kit of 遥 (char_4202_haruka, 6★ 护佑者; kit
// server/sim/content/kits/ops/op-haruka.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 BLS-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_haruka.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 { 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 ID = 'char_4202_haruka';
const FORMS = BACKUPS.units[ID].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const Y = 'uniequip_002_haruka';
const S1 = 'skchr_haruka_1', S2 = 'skchr_haruka_2', S3 = 'skchr_haruka_3';
const BUBBLE = 'haruka:bubble';
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
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_heavy: dummy('enemy_heavy', { mass: 4 }),
enemy_noflt: dummy('enemy_noflt', { immunities: { levitate: true } }),
};
const PLAIN_REC = chessRec({ id: 'test_plain_a', charId: 'char_test_plain', profession: 'CASTER', skill: null, stats: { maxHp: 2000, def: 0, res: 0 } });
const FORMS_ALL = [[5, false, null], [6, false, null], [5, true, null], [5, true, Y], [6, true, null], [6, true, Y]];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 遥 as uid 1 at (10, 5) 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_REC } }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'heal', 'statusApplied', 'attack'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: ID, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const ally = (uid, row, col) => ({ uid, chessId: 'test_plain_a', row, col });
/** Clear every bubble and make her next attack (or bubble) come at once — the battle start already gave one. */
function fresh(h, u) {
for (const x of h.b.allyUnits) h.b.removeBuff(x, BUBBLE);
u.atkCd = 0;
}
const bubbled = (h) => h.b.allyUnits.filter((a) => a.findBuff(BUBBLE)).map((a) => a.uid).sort();
const heals = (h, u, from = 0) => h.hooksOf('heal').slice(from).filter((x) => x.source === u && !x.opts?.regen);
test('遥 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, y-6, 护佑者 (ranged arts, hits air, blocks 1; BLS-Y 2 targets), S1 / S2 DEFAULT, S3 ACTIVE_RANGE on its y-8, ground-targetable, 空构 bond, no 特质', () => {
assert.equal(OPERATOR_KITS[ID], KITS[ID]);
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], [ID, 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, form.stats.res], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 护佑者`);
assert.equal(u.profile.maxTargets, mod === Y ? 2 : 1, `${label(f)}: targets`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-6`);
assert.equal(u.skill.rule, skill === 2 ? 'ACTIVE_RANGE' : 'DEFAULT', `${label(f)}: trigger`);
if (skill === 2) assert.deepEqual(u.skill.triggerGrid, form.skills[2].rangeGrid, `${label(f)}: the y-8 trigger grid`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
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], [1415, 398, 1728, 452]);
assert.deepEqual([modOf(5, Y).attr, modOf(6, Y).attr], [{ maxHp: 130, atk: 30 }, { maxHp: 210, atk: 52 }]);
});
test('a 自选 pick: 遥 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(ID));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(ID), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: ID, skillIndex: 1, uniEquipId: Y } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: ID, skillIndex: 1, uniEquipId: Y } } });
});
test('Trait: arts attacks (BLS-Y: two targets); while a skill runs every attack heals the most injured ally (BLS-Y: two) for 75 % of her ATK', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, Y]]) {
const { h, u } = field({ tier, elite, mod, skill: 0, others: [ally(2, 11, 6), ally(3, 9, 6)] });
h.spawn('enemy_dummy', { pos: [10, 7] }); h.spawn('enemy_fly', { pos: [11, 7] });
h.run(3);
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
assert.ok(hits.length && hits.every((c) => c.type === 'arts'));
assert.equal(new Set(hits.map((c) => c.target)).size, mod === Y ? 2 : 1, `${label([tier, elite, mod])}: targets`);
const [a, b] = [h.unit(2), h.unit(3)];
a.hp = 600; b.hp = 900;
const n0 = h.hooksOf('heal').length;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => heals(h, u, n0).length > 0, 4));
const first = heals(h, u, n0).filter((x, i, l) => Math.abs(x.t - l[0].t) < 1e-9);
assert.deepEqual(first.map((x) => x.target.uid).sort(), mod === Y ? [2, 3] : [2]);
for (const x of first) approx(x.amount, u.s.atk * 0.75, '75 % ATK');
done(h);
}
});
test('T1 浮光泡影: with each attack a bubble goes to the ally of her range with the lowest HP share that holds none (herself too); with nothing to attack the attack becomes the bubble, one per attack interval; 30 % 庇护 on physical and arts, not true; out of range nobody', () => {
const { h, u } = field({ others: [ally(2, 11, 6), ally(3, 9, 6), ally(4, 10, 9)] });
const [a, b, out] = [h.unit(2), h.unit(3), h.unit(4)];
a.hp = 1000; b.hp = 1200; out.hp = 100;
fresh(h, u);
// no enemy: ShieldOnly at once (a, 50 %), then one interval later b (60 %), then herself (full)
h.run(0.05);
assert.deepEqual(bubbled(h), [2]);
h.run(1.6);
assert.deepEqual(bubbled(h), [2, 3]);
h.run(1.6);
assert.deepEqual(bubbled(h), [1, 2, 3]);
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u).length, 0, 'no attack');
h.run(3.2);
assert.deepEqual(bubbled(h), [1, 2, 3], 'nobody left in range (the one at (10, 9) is outside)');
const bub = a.findBuff(BUBBLE);
approx(bub.data.v, 0.3, '30 %', 1e-9);
const e = h.spawn('enemy_dummy', { pos: [11, 9] });
for (const [type, want] of [['phys', 70], ['arts', 70], ['true', 100]]) {
const hp0 = b.hp;
// a 无来源 hit: the bubble takes no notice
h.b.dealDamage(e, b, { amount: 100, type, canDodge: false, sourceless: true });
approx(hp0 - b.hp, want, `${type}`);
b.hp = 1200;
}
assert.ok(b.findBuff(BUBBLE), '无来源 damage never breaks a bubble');
// with a target: a bubble after each attack
for (const x of [a, b, u]) h.b.removeBuff(x, BUBBLE);
h.spawn('enemy_dummy', { pos: [10, 7] });
const n0 = h.hooksOf('attack').length;
assert.ok(h.runUntil(() => h.hooksOf('attack').slice(n0).some((c) => c.attacker === u), 2));
h.step();
assert.deepEqual(bubbled(h), [2], 'the attack and its bubble');
done(h);
});
test('T1 / T2: an enemy hit that lowers the holder\'s HP breaks its bubble 0.5 s later (once; a fully shielded hit does not count) and heals it 25 % of her ATK as it was when she gave it; BLS-Y stage 3: 20 % to give the holder 1 SP; stage 1: never', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, Y], [5, true, Y]]) {
const { h, u } = field({ tier, elite, mod, others: [ally(2, 11, 6)] });
const a = h.unit(2);
a.hp = 1000;
fresh(h, u);
h.run(0.05);
const b = a.findBuff(BUBBLE);
assert.ok(b, 'bubbled');
const atk0 = u.s.atk;
h.b.addBuff(u, { key: 'test:atk', mods: { atkPct: 1 } }); // her ATK doubles after the grant
const e = h.spawn('enemy_dummy', { pos: [11, 9] });
h.b.addBuff(a, { key: 'test:shield', shield: 500 });
h.b.dealDamage(e, a, { amount: 300, type: 'true' });
h.run(0.6);
assert.equal(a.findBuff(BUBBLE), b, 'a hit the shield took whole: no break');
h.b.removeBuff(a, 'test:shield');
const n0 = h.hooksOf('heal').length;
h.b.dealDamage(e, a, { amount: 100, type: 'true' });
h.run(0.25);
h.b.dealDamage(e, a, { amount: 100, type: 'true' });
h.run(0.2);
assert.equal(a.findBuff(BUBBLE), b, 'still there at 0.45 s');
h.run(0.1);
assert.notEqual(a.findBuff(BUBBLE), b, 'broken 0.5 s after the first hit');
const hs = heals(h, u, n0).filter((x) => x.target === a);
assert.equal(hs.length, 1, 'one break, one heal');
approx(hs[0].amount, atk0 * 0.25, '25 % of the ATK of the grant');
void u;
done(h);
}
// BLS-Y stage 3: the dice
for (const [tier, want] of [[6, true], [5, false]]) {
const { h, u } = field({ tier, elite: true, mod: Y, others: [{ uid: 2, chessId: 'chess_char_1_08_a', row: 11, col: 6 }] });
const tex = h.unit(2);
const e = h.spawn('enemy_dummy', { pos: [11, 9] });
let gained = 0, breaks = 0;
h.b.on('spGain', (c) => { if (c.unit === tex && c.reason === 'talent') gained += c.amount; });
for (let i = 0; i < 300; i++) {
tex.skill.sp = 0; tex.skill.charges = 0;
if (!tex.findBuff(BUBBLE)) h.run(0.05);
if (!tex.findBuff(BUBBLE)) { h.run(1.7); continue; }
tex.hp = tex.s.maxHp * 0.5;
h.b.dealDamage(e, tex, { amount: 1, type: 'true' });
h.run(0.55);
breaks++;
}
if (want) assert.ok(gained / breaks > 0.12 && gained / breaks < 0.28, `≈ 20 %: ${gained} / ${breaks}`);
else assert.equal(gained, 0, 'stage 1: no SP');
void u;
done(h);
}
});
test('S1 夜啼彩羽 (DEFAULT): ASPD +30 / +45 for 19 / 20 s, heals; a bubble breaking meanwhile at once gives a new one to the ally of the 8 around its holder that has none, the lowest HP share first', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const { h, u } = field({ tier, elite, skill: 0, others: [ally(2, 11, 6), ally(3, 12, 6), ally(4, 12, 7), ally(5, 9, 9)] });
assert.deepEqual([sk.duration, sk.bb.attack_speed, u.skill.spCost, sk.initSp], [elite ? 20 : 19, elite ? 45 : 30, 25, elite ? 10 : 7]);
const [a, b, c] = [h.unit(2), h.unit(3), h.unit(4)];
h.spawn('enemy_dummy', { pos: [10, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
approx(u.skill.timeLeft, sk.duration, 'duration', 0.05);
approx(u.s.aspd, 100 + sk.bb.attack_speed, 'ASPD');
for (const x of h.b.allyUnits) h.b.removeBuff(x, BUBBLE);
h.b.addBuff(a, { key: BUBBLE, source: u, data: { v: 0.3, atk: u.s.atk, heal: 0.25, prob: 0, sp: 0, boom: false } });
b.hp = 1500; c.hp = 1200; // both around a; c has the lower share
const e = h.spawn('enemy_dummy', { pos: [11, 9] });
h.b.dealDamage(e, a, { amount: 10, type: 'true' });
h.run(0.52);
assert.ok(!a.findBuff(BUBBLE) && c.findBuff(BUBBLE), 'the new bubble on the lowest share of the 8 around');
assert.equal(c.findBuff(BUBBLE).source, u);
h.runUntil(() => !u.skill.active, 25);
approx(u.s.aspd, 100, 'back');
done(h);
}
});
test('S2 幽隙栖萤: every heal of hers meanwhile deals heal × 140 % / 200 % arts damage to the 2 / 3 nearest enemies within 1.7 of the healed unit (air units too); rank 7: one more heal target and one more bubble; the first use lasts 22 / 23 s with no ATK bonus, the next ones ATK +20 % / +25 % and endless; the count restarts with a deployment', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.duration, sk.bb.atk_scale_extra, sk.bb.max_target_extra, sk.bb['attack@max_target_heal_add'], sk.bb.max_target_shield_add, sk.bb.atk, sk.bb.ability_range_radius],
[elite ? 23 : 22, elite ? 2 : 1.4, elite ? 3 : 2, elite ? 1 : 0, elite ? 1 : 0, elite ? 0.25 : 0.2, 1.7]);
const { h, u } = field({ tier, elite, skill: 1, others: [ally(2, 11, 6), ally(3, 9, 6), ally(4, 10, 4)] });
const [a, b] = [h.unit(2), h.unit(3)];
h.spawn('enemy_dummy', { pos: [9, 7] }); // her target, √5 from a
const near = [h.spawn('enemy_dummy', { pos: [12, 6] }), h.spawn('enemy_fly', { pos: [11, 7] }), h.spawn('enemy_dummy', { pos: [12, 7] })];
h.spawn('enemy_dummy', { pos: [13, 9] }); // too far from (11, 6)
h.run(0.1);
for (const x of h.b.allyUnits) h.b.removeBuff(x, BUBBLE);
a.hp = 400; b.hp = 1000;
const n0 = h.hooksOf('heal').length;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
approx(u.s.atk, u.base.atk, 'first use: no ATK bonus');
assert.ok(h.runUntil(() => heals(h, u, n0).length > 0, 3));
const t0 = heals(h, u, n0)[0].t;
const first = heals(h, u, n0).filter((x) => Math.abs(x.t - t0) < 1e-9);
assert.deepEqual(first.map((x) => x.target.uid).sort(), elite ? [2, 3] : [2], 'rank 7: two heal targets');
const ha = first.find((x) => x.target === a);
const dmg = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.tags?.includes('haruka:s2') && Math.abs(c.t - t0) < 1e-9);
// around a (11, 6): the three near enemies; (12, 7) is √2 ≈ 1.41 away — the 2 nearest at rank 4 are (12, 6) and (11, 7)
const onA = dmg.filter((c) => near.includes(c.target));
assert.equal(new Set(onA.map((c) => c.target)).size, elite ? 3 : 2, 'max_target_extra');
for (const c of onA) approx(c.amount, ha.amount * sk.bb.atk_scale_extra, `heal × ${sk.bb.atk_scale_extra}`);
assert.ok(onA.some((c) => c.target === near[1]), 'the flyer too');
assert.ok(onA.every((c) => c.type === 'arts'));
// rank 7: two bubbles per attack
h.step();
assert.ok(bubbled(h).length >= (elite ? 2 : 1), 'bubbles per attack');
h.runUntil(() => !u.skill.active, 30);
approx(h.hooksOf('skillEnd').filter((c) => c.unit === u).at(-1).t - h.hooksOf('skillStart').filter((c) => c.unit === u).at(-1).t, sk.duration, 'first use: its duration', 0.05);
// second use: ATK bonus, endless
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 4));
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), 'second use: ATK');
h.run(sk.duration * 3);
assert.ok(u.skill.active, 'endless');
// a new deployment: the first use again
h.b.retreat(u);
h.b.redeploy(u);
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 4));
approx(u.s.atk, u.base.atk, 'first use of the new deployment');
done(h);
}
});
test('S3 夏末游鳞 (ACTIVE_RANGE): cast with an enemy on its y-8 outside her y-6; range y-8 while on, back after; ATK +25 % / +40 %; interval 1.6 − 0.4 / 0.6 s; heals; her bubbles ×1.4 / ×1.7 (also those given before); an enemy setting one off is 浮空 3 / 4 s (half on 重量 > 3, none when immune) and takes 60 % / 70 % ATK arts each second of it (from 0.5 s)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const form = formOf(tier, elite);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.bb.damage_resistance_scale, sk.bb.levitate_duration, sk.bb.atk_scale, sk.bb.interval],
[40, elite ? 0.4 : 0.25, elite ? -0.6 : -0.4, elite ? 1.7 : 1.4, elite ? 4 : 3, elite ? 0.7 : 0.6, 1]);
const { h, u } = field({ tier, elite, skill: 2, others: [ally(2, 11, 6), ally(3, 9, 6)] });
const a = h.unit(2);
h.run(0.1);
assert.ok(a.findBuff(BUBBLE) || h.unit(3).findBuff(BUBBLE) || u.findBuff(BUBBLE), 'bubbles before');
const holder = [a, h.unit(3), u].find((x) => x.findBuff(BUBBLE));
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, 'no enemy: no cast');
h.spawn('enemy_dummy', { pos: [10, 8] }); // y-8 (+3), not y-6
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast on the y-8');
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, 'y-8 while on');
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), 'ATK');
approx(u.s.interval, 1.6 + sk.bb.base_attack_time, 'interval');
approx(holder.findBuff(BUBBLE).data.v, 0.3 * sk.bb.damage_resistance_scale, 'a bubble given before: rescaled', 1e-9);
// levitation: a hit on a bubbled ally
for (const x of h.b.allyUnits) h.b.removeBuff(x, BUBBLE);
h.b.addBuff(a, { key: BUBBLE, source: u, data: { v: 0.42, atk: u.s.atk, heal: 0, prob: 0, sp: 0, boom: false } });
const e = h.spawn('enemy_dummy', { pos: [12, 9] });
h.b.dealDamage(e, a, { amount: 50, type: 'true' });
const lev = h.hooksOf('statusApplied').filter((c) => c.target === e && c.status === 'levitate');
assert.equal(lev.length, 1);
approx(lev[0].duration, sk.bb.levitate_duration, 'levitate');
assert.ok(e.isFlying, 'an air unit meanwhile');
const n0 = h.hooksOf('damaged').length;
h.run(sk.bb.levitate_duration + 0.2);
const ticks = h.hooksOf('damaged').slice(n0).filter((c) => c.target === e && c.dmg?.tags?.includes('haruka:float'));
assert.equal(ticks.length, sk.bb.levitate_duration, 'one tick a second: 0.5, 1.5 …');
approx(ticks[0].t - lev[0].t, 0.5, 'first tick at 0.5 s', 0.05);
for (const c of ticks) { approx(c.amount, u.s.atk * sk.bb.atk_scale, `${sk.bb.atk_scale * 100} % ATK`); assert.equal(c.type, 'arts'); }
// heavy: half; immune: none
const heavy = h.spawn('enemy_heavy', { pos: [12, 10] }), immune = h.spawn('enemy_noflt', { pos: [12, 11] });
h.b.addBuff(a, { key: BUBBLE, source: u, data: { v: 0.42, atk: u.s.atk, heal: 0, prob: 0, sp: 0, boom: false } });
h.b.dealDamage(heavy, a, { amount: 10, type: 'true' });
h.b.dealDamage(immune, a, { amount: 10, type: 'true' });
approx(h.hooksOf('statusApplied').filter((c) => c.target === heavy && c.status === 'levitate')[0].duration, sk.bb.levitate_duration / 2, '重量 > 3: half');
assert.equal(h.hooksOf('statusApplied').filter((c) => c.target === immune && c.status === 'levitate').length, 0, 'immune');
h.run(2);
assert.equal(h.hooksOf('damaged').filter((c) => c.target === immune && c.dmg?.tags?.includes('haruka:float')).length, 0, 'no damage without the levitation');
h.runUntil(() => !u.skill.active, 45);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, 'back to y-6');
approx(u.s.interval, 1.6, 'interval back');
done(h);
}
});
test('outside S3 a bubble levitates nobody; a bubble outlasts her leaving and still heals with the ATK of its grant', () => {
const { h, u } = field({ skill: 0, others: [ally(2, 11, 6)] });
const a = h.unit(2);
a.hp = 1000;
fresh(h, u);
h.run(0.05);
assert.ok(a.findBuff(BUBBLE));
const atk = u.s.atk;
const e = h.spawn('enemy_dummy', { pos: [11, 9] });
h.b.retreat(u);
h.step();
assert.ok(a.findBuff(BUBBLE), 'still there');
const n0 = h.hooksOf('heal').length;
h.b.dealDamage(e, a, { amount: 10, type: 'true' });
assert.equal(h.hooksOf('statusApplied').filter((c) => c.status === 'levitate').length, 0);
h.run(0.6);
approx(heals(h, u, n0)[0].amount, atk * 0.25, 'her heal after she left');
done(h);
});
+430
View File
@@ -0,0 +1,430 @@
// test/content/op_phatm2.test.js — the 自选 operator kit of 酒神 (char_1042_phatm2, 6★ 巫役; kit
// server/sim/content/kits/ops/op-phatm2.js) and of her summons 本能的召唤 (token_10054_phatm2_encdool) and 迷狂牢笼
// (token_10055_phatm2_mndclv), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy; 本能的召唤 as the
// placed hand piece of her player) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60,
// rank 7) with no module or RIT-X “酒神之心” 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_phatm2.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 ID = 'char_1042_phatm2';
const CALL = 'token_10054_phatm2_encdool', CAGE = 'token_10055_phatm2_mndclv';
const FORMS = BACKUPS.units[ID].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_phatm2';
const S1 = 'skchr_phatm2_1', S2 = 'skchr_phatm2_2', S3 = 'skchr_phatm2_3';
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
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_elite: dummy('enemy_elite', { rank: 'ELITE' }),
enemy_shooter: dummy('enemy_shooter', { atk: 300, range: 2, bat: 1 }),
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 1, mass: 0, atk: 50 }),
enemy_walk_elite: enemyRec({ key: 'enemy_walk_elite', hp: 1e9, speed: 1, mass: 0, atk: 50, rank: 'ELITE' }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], [5, true, null], [5, true, X], [6, true, null], [6, true, X]];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 酒神 as uid 1 at (10, 5) facing RIGHT, plus `others`; `call` = the 本能的召唤 piece's tile (uid 9) or null. */
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], call = null, callFirst = false, seed = 5 } = {}) {
const op = { uid: 1, diy: { slot: SLOT[tier], charId: ID, skillIndex: skill, uniEquipId: mod }, elite, row, col };
const piece = call ? [{ uid: 9, kind: 'token', tokenId: CALL, ownerUid: 1, row: call[0], col: call[1] }] : [];
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack', 'elementBurst'], captureNoisy: true,
units: callFirst ? [...piece, op, ...others] : [op, ...piece, ...others],
});
h.step();
return { h, u: h.unit(1), callUnit: h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === CALL) ?? null };
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const elemOf = (h, src, from = 0) => h.hooksOf('damaged').slice(from).filter((c) => c.source === src && c.type === 'element');
const cagesOf = (h) => h.b.allyUnits.filter((a) => a.kind === 'token' && a.defId === CAGE && a.alive);
/** No attacks of hers (to isolate an effect). */
const quiet = (h, u) => h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
test('酒神 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, y-2, 巫役 (ranged arts, hits air, blocks 1), S1 AUTO on attack SP (data DEFAULT), S2 at full SP (kit), S3 ACTIVE_RANGE on its y-8, ground-targetable, 维多利亚 bond, no 特质', () => {
assert.equal(OPERATOR_KITS[ID], KITS[ID]);
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], [ID, 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, form.stats.res], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat, u.profile.maxTargets], [1, 'ranged', 'arts', true, 1.6, 1], `${label(f)}: 巫役`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-2`);
assert.deepEqual([u.skill.rule, u.skill.spType, u.skill.kind], [['DEFAULT', 'attack', 'instant'], ['SP_FULL', 'time', 'toggle'], ['ACTIVE_RANGE', 'time', 'duration']][skill], `${label(f)}: trigger / SP / kind`);
if (skill === 2) assert.deepEqual(u.skill.triggerGrid, form.skills[2].rangeGrid, `${label(f)}: the y-8 trigger grid`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
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], [936, 411, 1111, 463]);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr], [{ maxHp: 125, atk: 30 }, { maxHp: 200, atk: 42 }]);
});
test('a 自选 pick: 酒神 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(ID));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(ID), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: ID, skillIndex: 2, uniEquipId: X } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: ID, skillIndex: 2, uniEquipId: X } } });
});
test('T1 形为心役: every hit of her attacks first gives its target 30 % ATK 神经损伤 and every other enemy within 1.3 of it (中点, air too) 20 % ATK — then the arts damage; one 1.5 away nothing', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const { h, u } = field({ tier, elite, skill: 2 });
const t1 = formOf(tier, elite).talents[0].bb;
assert.deepEqual(t1, { 'attack@ep_damage_ratio': 0.3, ep_damage_ratio: 0.2, range_radius: 1.3 });
const main = h.spawn('enemy_dummy', { pos: [10, 6] });
const nearFly = h.spawn('enemy_fly', { pos: [11, 6] });
const far = h.spawn('enemy_dummy', { pos: [11.5, 7] }); // ≈ 1.8 from (10, 6)
const n0 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => h.hooksOf('damaged').slice(n0).some((c) => c.source === u && c.dmg?.isAttack && c.target === main), 3));
const seq = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u);
const iHit = seq.findIndex((c) => c.dmg?.isAttack && c.target === main);
const before = seq.slice(0, iHit);
const mainEl = before.find((c) => c.type === 'element' && c.target === main);
const flyEl = before.find((c) => c.type === 'element' && c.target === nearFly);
assert.ok(mainEl && flyEl, '神经损伤 before the damage');
assert.equal(mainEl.dmg.element, 'neural');
approx(mainEl.amount, u.s.atk * 0.3, '30 % ATK');
approx(flyEl.amount, u.s.atk * 0.2, '20 % ATK to the flyer around');
assert.ok(!before.some((c) => c.target === far), 'outside the 1.3');
approx(seq[iHit].amount, u.s.atk, 'arts damage = ATK');
assert.equal(seq[iHit].type, 'arts');
done(h);
}
});
test('RIT-X “酒神之心”: her 神经损伤 on an ELITE / leader enemy ×1.18 (stages 1 and 3), on a normal one ×1; none without the module', () => {
for (const [tier, elite, mod] of [[5, true, X], [6, true, X], [6, true, null], [5, false, null]]) {
const { h, u } = field({ tier, elite, mod, skill: 2 });
const want = mod ? 1.18 : 1;
if (mod) assert.equal(modOf(tier, mod).traitOverride.bb.ep_damage_scale, 1.18);
const el = h.spawn('enemy_elite', { pos: [10, 6] });
const n0 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => elemOf(h, u, n0).some((c) => c.target === el), 3));
approx(elemOf(h, u, n0).find((c) => c.target === el).amount, u.s.atk * 0.3 * want, `${label([tier, elite, mod])}: elite`);
h.b.kill(el);
const nm = h.spawn('enemy_dummy', { pos: [10, 6] });
const n1 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => elemOf(h, u, n1).some((c) => c.target === nm), 3));
approx(elemOf(h, u, n1).find((c) => c.target === nm).amount, u.s.atk * 0.3, 'normal enemy');
done(h);
}
});
test('T2 堕梦: while she is on the field every enemy in a 神经损伤 burst has ASPD −12 (RIT-X stage 3 −20), on the whole field; none outside the burst or once she left', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X], [5, true, X]]) {
const { h, u } = field({ tier, elite, mod, skill: 2 });
quiet(h, u);
const v = formOf(tier, elite).talents[1].bb.attack_speed + (mod === X && tier === 6 ? -8 : 0);
const e = h.spawn('enemy_dummy', { pos: [12, 15] }); // far outside her range
const aspd0 = e.s.aspd;
e.elem.neural = 999;
h.b.dealDamage(u, e, { type: 'element', element: 'neural', amount: 10 });
assert.ok(e.findBuff('neuralBurst'), 'burst');
h.step();
approx(e.s.aspd, aspd0 + v, `ASPD ${v}`);
h.b.retreat(u);
h.run(0.3);
approx(e.s.aspd, aspd0, 'she left: gone');
done(h);
}
});
test('T2 堕梦: an enemy of her range starting a normal attack first takes 70 (RIT-X stage 3: 90) 神经损伤; a burst from it interrupts that very attack (麻痹); outside her range nothing', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X]]) {
const { h, u } = field({ tier, elite, mod, skill: 2 });
quiet(h, u);
const val = mod === X && tier === 6 ? 90 : 70;
const sh = h.spawn('enemy_shooter', { pos: [10, 7] }); // in her y-2, 2 tiles from her
const n0 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => h.hooksOf('damaged').slice(n0).some((c) => c.source === sh && c.target === u), 3), 'it shoots her');
const el = elemOf(h, u, n0).filter((c) => c.target === sh);
assert.equal(el.length, 1, 'one 神经损伤 per attack');
approx(el[0].amount, val, `${val}`);
// a burst from it: the attack is interrupted
sh.elem.neural = 1000 - val + 1;
const hp0 = u.hp, n1 = h.hooksOf('damaged').length;
h.run(1.05);
assert.ok(sh.findBuff('neuralBurst'), 'burst on its attack');
assert.ok(!h.hooksOf('damaged').slice(n1).some((c) => c.source === sh && c.target === u && c.dmg?.isAttack), 'that attack never lands');
assert.equal(u.hp, hp0);
// outside her range
const out = h.spawn('enemy_shooter', { pos: [12, 8] });
h.b.relocate(u, 10, 5);
const n2 = h.hooksOf('damaged').length;
h.run(3);
assert.ok(!elemOf(h, u, n2).some((c) => c.target === out), '(12, 8) is outside her y-2');
done(h);
}
});
test('S1 暗夜回声 (AUTO, 3 attack SP): every 4th attack hits twice for 105 % / 120 % ATK arts, an enemy not in a burst first, and roots it 2 / 3 s; while that root holds 神经损伤 on it ×1.3 / ×1.5 (from anyone)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.spCost, sk.initSp, sk.bb.atk_scale, sk.bb.times, sk.bb.unmove, sk.bb.ep_damage_scale], [3, 0, elite ? 1.2 : 1.05, 2, elite ? 3 : 2, elite ? 1.5 : 1.3]);
const { h, u } = field({ tier, elite, skill: 0 });
const burst = h.spawn('enemy_dummy', { pos: [10, 6] });
const fresh = h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.addBuff(burst, { key: 'neuralBurst', duration: 100, flags: { burstLock: true } });
assert.ok(h.runUntil(() => u.skill.activations === 1, 10));
h.run(0.5); // the bolts land
const atks = h.hooksOf('attack').filter((c) => c.attacker === u);
assert.deepEqual(atks.map((c) => c.isSkill), [false, false, false, true], 'three plain attacks, then the skill\'s');
const skillHits = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack && c.dmg.isSkill);
assert.equal(skillHits.length, 2, 'two hits');
assert.ok(skillHits.every((c) => c.target === fresh), 'the enemy not in a burst');
for (const c of skillHits) approx(c.amount, u.s.atk * sk.bb.atk_scale, `${sk.bb.atk_scale * 100} %`);
const bind = h.hooksOf('statusApplied').find((c) => c.status === 'bind' && c.target === fresh);
assert.ok(bind, 'rooted');
approx(bind.duration, sk.bb.unmove, 'root time');
const n0 = h.hooksOf('damaged').length;
const other = h.spawn('enemy_dummy', { pos: [12, 12] });
h.b.dealDamage(other, fresh, { type: 'element', element: 'neural', amount: 100 });
approx(h.hooksOf('damaged').slice(n0).find((c) => c.target === fresh && c.type === 'element').amount, 100 * sk.bb.ep_damage_scale, `×${sk.bb.ep_damage_scale} while rooted`);
h.run(sk.bb.unmove + 0.1);
const n1 = h.hooksOf('damaged').length;
h.b.dealDamage(other, fresh, { type: 'element', element: 'neural', amount: 100 });
approx(h.hooksOf('damaged').slice(n1).find((c) => c.target === fresh && c.type === 'element').amount, 100, '×1 after it');
done(h);
}
});
test('S2 群体性谵妄 (AUTO, at full SP, endless): cast after 33 / 30 s with no enemy; ASPD +20 / +25; T1\'s splash radius 1.5; it never ends', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.spCost, sk.initSp, sk.bb.attack_speed, sk.bb['talent@range_radius']], [elite ? 30 : 33, 0, elite ? 25 : 20, 1.5]);
const { h, u } = field({ tier, elite, skill: 1 });
h.run(sk.spCost - 0.5);
assert.equal(u.skill.activations, 0);
assert.ok(h.runUntil(() => u.skill.active, 1), 'at full SP');
approx(u.s.aspd, 100 + sk.bb.attack_speed, 'ASPD');
const main = h.spawn('enemy_dummy', { pos: [10, 6] });
const at14 = h.spawn('enemy_dummy', { pos: [11.4, 6] }); // 1.4 away: inside 1.5, outside 1.3
const n0 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => elemOf(h, u, n0).some((c) => c.target === at14), 3), 'the 1.5 splash');
void main;
h.run(200);
assert.ok(u.skill.active, 'endless');
done(h);
}
});
test('S2 + 本能的召唤: the placed piece deploys at the battle start (untargetable, no attack, no block), 诱导s the enemies entering its x-1 (at most 4, ELITE first), leaves 10 s after or when one reaches it; leaving: every enemy of its x-1 停顿 6 s and 12 × (15 % / 20 % ATK 神经损伤 + 100 % / 120 % ATK arts) over 6 s at her ATK of its deployment; back with S2 and again 25 s after each exit while it runs; her leaving withdraws it', () => {
for (const [tier, elite, callFirst] of [[5, false, false], [6, true, true]]) {
const sk = skillOf(tier, elite, S2);
const { h, u, callUnit } = field({ tier, elite, skill: 1, call: [10, 6], callFirst });
const call = callUnit;
assert.ok(call && call.alive, 'deployed with the board');
assert.ok(call.s.flags.untargetable && call.profile.noAttack && call.s.blockCnt === 0, 'untargetable, no attack, no block');
const atkAtDeploy = u.s.atk;
// nothing to lure: it leaves after 10 s; the enemies of its x-1 then get the 停顿 and the damage
const inside = h.spawn('enemy_dummy', { pos: [11, 7] });
h.b.addBuff(inside, { key: 'test:noattract', flags: { attract: true } }); // (already lured elsewhere: not taken)
h.b.addBuff(u, { key: 'test:atk', mods: { atkPct: 1 } }); // her ATK changes after the deployment
quiet(h, u);
assert.ok(h.runUntil(() => !call.alive, 11), 'leaves after 10 s');
approx(h.hooksOf('death').find((c) => c.unit === call).t, 10, '10 s', 0.05);
h.b.removeBuff(inside, 'test:noattract');
const slow = h.hooksOf('statusApplied').find((c) => c.status === 'sluggish' && c.target === inside);
assert.ok(slow);
approx(slow.duration, sk.bb.sluggish, '停顿 6 s');
const n0 = h.hooksOf('damaged').length;
h.run(6.2);
const ticks = h.hooksOf('damaged').slice(n0).filter((c) => c.target === inside && c.source === u);
const arts = ticks.filter((c) => c.type === 'arts'), neural = ticks.filter((c) => c.type === 'element');
assert.equal(arts.length, 12, '12 arts ticks');
// 12 神经损伤 ticks — unless the gauge bursts first (then the 爆发冷却 takes no more: elite 12 × 92.6 > 1000)
const per = atkAtDeploy * sk.bb.ep_damage_ratio_token;
assert.equal(neural.length, Math.min(12, Math.ceil(1000 / per)), '神经损伤 ticks');
for (const c of arts) approx(c.amount, atkAtDeploy * sk.bb.atk_scale, `arts ${sk.bb.atk_scale * 100} % of the cached ATK`);
for (const c of neural) approx(c.amount, atkAtDeploy * sk.bb.ep_damage_ratio_token, `神经损伤 ${sk.bb.ep_damage_ratio_token * 100} %`);
assert.ok(arts.every((c) => c.dmg.tags.includes('dot') && c.dmg.canDodge === false), '持续伤害');
h.b.removeBuff(u, 'test:atk');
h.b.kill(inside);
// S2 (at full SP: 33 / 30 s) gives it back, after its own 25 s redeploy time from its exit at 10 s
assert.ok(h.runUntil(() => u.skill.active, 20));
approx(h.b.time, sk.spCost, 'S2 at full SP', 0.05);
assert.ok(!call.alive, 'still in its redeploy time');
assert.ok(h.runUntil(() => call.alive, 6), 'back with S2');
approx(h.b.time, 35, 'at 10 + 25 s', 0.6);
const t0 = h.b.time;
// five walkers on row 9 enter its x-1 at (9, 7) together: four are lured, the elite first; the first to arrive ends it
const w = [h.spawn('enemy_walk', { routeIndex: 0 }), h.spawn('enemy_walk', { routeIndex: 0 }), h.spawn('enemy_walk', { routeIndex: 0 }),
h.spawn('enemy_walk', { routeIndex: 0 }), h.spawn('enemy_walk_elite', { routeIndex: 0 })];
assert.ok(h.runUntil(() => !call.alive, 12), 'gone again');
const lured = h.hooksOf('statusApplied').filter((c) => c.status === 'attract' && c.source === call && c.t >= t0);
assert.equal(lured.length, 4, 'max_target 4');
assert.equal(lured[0].target, w[4], 'the elite first');
assert.equal(new Set(lured.map((c) => c.target)).size, 4);
const exitT = h.hooksOf('death').filter((c) => c.unit === call).at(-1).t;
assert.ok(exitT - t0 < 10 - 0.5, `left on an arrival, before its 10 s (${(exitT - t0).toFixed(2)} s)`);
assert.ok(lured.some((c) => Math.hypot(c.target.x - 6, c.target.y - 10) <= 0.4 + 1e-6), 'one of them within 0.4 of it');
assert.ok(w.every((e) => !e.findBuff('attract') || e.findBuff('attract').source !== call), 'the 诱导 ends with it');
// 25 s later it is back (S2 still runs)
h.run(24.5);
assert.ok(!call.alive);
assert.ok(h.runUntil(() => call.alive, 1.5), 'back after its own redeploy time');
h.b.retreat(u);
h.step();
assert.ok(!call.alive, 'withdrawn when she leaves');
h.run(30);
assert.ok(!call.alive, 'not back without her');
done(h);
}
});
test('本能的召唤: an ELITE in its x-1 is lured before normal enemies; only reachable ground enemies (flyers always)', () => {
const { h, u, callUnit } = field({ skill: 1, call: [11, 6] });
quiet(h, u);
const n = [h.spawn('enemy_dummy', { pos: [12, 6] }), h.spawn('enemy_dummy', { pos: [11, 5] }), h.spawn('enemy_dummy', { pos: [11, 7] }),
h.spawn('enemy_dummy', { pos: [10, 6] }), h.spawn('enemy_elite', { pos: [12, 7] })];
h.run(0.15);
const lured = h.hooksOf('statusApplied').filter((c) => c.status === 'attract' && c.source === callUnit).map((c) => c.target);
assert.equal(lured.length, 4, 'four');
assert.equal(lured[0], n[4], 'the elite first');
done(h);
});
test('S3 空剧场 (ACTIVE_RANGE on y-8): range y-8 while on, ATK +75 % / +95 %, an enemy not in a burst first; her 神经损伤 starts 10 % ATK 神经损伤 a second (from 0.1 s) until a burst; the 爆发冷却 of enemies in her range runs 1.5×', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const form = formOf(tier, elite);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.ep_damage_ratio, sk.bb.interval, sk.bb['talent@ep_break_recover_speed'], sk.bb['phatm2_s_3[token_cooldown].interval']],
[30, elite ? 0.95 : 0.75, 0.1, 1, 0.5, 30]);
const { h, u } = field({ tier, elite, skill: 2 });
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, 'no enemy: no cast');
const e = h.spawn('enemy_dummy', { pos: [10, 8] }); // y-8 (+3), not y-2
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast on the y-8');
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, 'y-8 while on');
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), 'ATK');
const n0 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => elemOf(h, u, n0).some((c) => c.target === e), 3), 'her first 神经损伤');
const t0 = elemOf(h, u, n0).find((c) => c.target === e).t;
quiet(h, u);
h.run(2.2);
const dot = elemOf(h, u, n0).filter((c) => c.target === e && c.dmg.tags.includes('phatm2:theatre'));
assert.ok(dot.length >= 2, `${dot.length} ticks`);
approx(dot[0].t - t0, 0.1, 'first at 0.1 s', 0.05);
approx(dot[1].t - dot[0].t, 1, 'then each second', 0.05);
for (const c of dot) approx(c.amount, u.s.atk * 0.1, '10 % ATK');
// until a burst
e.elem.neural = 999;
h.run(1.1);
assert.ok(e.findBuff('neuralBurst'), 'burst');
const n1 = h.hooksOf('damaged').length;
h.run(3);
assert.equal(elemOf(h, u, n1).filter((c) => c.target === e && c.dmg.tags.includes('phatm2:theatre')).length, 0, 'stops in a burst');
// the 爆发冷却 runs 1.5× in her range
const lock = e.findBuff('neuralBurst');
const left = lock.timeLeft;
h.run(1);
approx(left - lock.timeLeft, 1.5, 'cooldown 1.5×', 0.05);
h.runUntil(() => !u.skill.active, 35);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, 'back to y-2');
done(h);
}
});
test('S3 迷狂牢笼: a 神经损伤 burst of an enemy in her range puts a 牢笼 on its ground tile that blocks the enemies on it (air too), takes 1 per hit from them only, falls after 3 hits; it takes nobody else; refreshed by a burst on its tile; leaves with nobody blocked, 30 s after the skill, or when she leaves', () => {
const { h, u } = field({ skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 8] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
quiet(h, u);
const a = h.spawn('enemy_shooter', { pos: [11, 7] }), fly = h.spawn('enemy_fly', { pos: [11, 7] });
a.elem.neural = 990;
h.b.dealDamage(u, a, { type: 'element', element: 'neural', amount: 20 });
const cage = cagesOf(h)[0];
assert.ok(cage, 'a 牢笼');
assert.deepEqual([cage.tileR, cage.tileC, cage.ownerUnit === u], [11, 7, true]);
assert.ok(a.blockedBy === cage && fly.blockedBy === cage, 'both blocked, the flyer too');
assert.equal(cage.s.blockCnt, 2, 'block count = their weight');
assert.ok(cage.s.flags.isolated && cage.s.flags.noHeal && cage.profile.noAttack, '孤立, 禁疗, no attack');
// damage: from another enemy nothing, from a blocked one 1
const other = h.spawn('enemy_dummy', { pos: [12, 12] });
h.b.dealDamage(other, cage, { amount: 500, type: 'phys' });
assert.equal(cage.hp, 3, 'immune to an enemy it does not block');
h.b.dealDamage(null, cage, { amount: 500, type: 'true', sourceless: true });
assert.equal(cage.hp, 3, '无来源: nothing');
h.b.dealDamage(fly, cage, { amount: 500, type: 'phys' });
assert.equal(cage.hp, 2, '1 per hit');
// an enemy arriving on its tile is not taken by contact
const late = h.spawn('enemy_dummy', { pos: [11, 7] });
h.run(0.2);
assert.equal(late.blockedBy, null, 'no new block by contact');
// refreshed by its 神经损伤 burst on that tile: HP back, it is blocked too
late.elem.neural = 999;
h.b.dealDamage(u, late, { type: 'element', element: 'neural', amount: 10 });
assert.equal(late.blockedBy, cage, 'refreshed: blocked');
assert.equal(cage.hp, 3, 'full HP again');
assert.equal(cage.s.blockCnt, 3);
assert.equal(cagesOf(h).length, 1, 'no second 牢笼 on the tile');
// three hits from a blocked enemy: gone, the enemies released
for (let i = 0; i < 3; i++) h.b.dealDamage(fly, cage, { amount: 100, type: 'phys' });
assert.ok(!cage.alive, 'three hits');
assert.ok(!a.blockedBy && !fly.blockedBy, 'released');
done(h);
});
test('S3 迷狂牢笼: no 牢笼 on a tile with a unit or on high ground; at the skill start the enemies of her range already in a burst set T1\'s splash off once and get one; 牢笼 last 30 s after the skill; her leaving withdraws them', () => {
const { h, u } = field({ skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 11, col: 6 }] });
quiet(h, u);
// already bursting before the skill
const pre = h.spawn('enemy_dummy', { pos: [10, 7] }), side = h.spawn('enemy_dummy', { pos: [10.9, 7] });
pre.elem.neural = 999;
h.b.dealDamage(u, pre, { type: 'element', element: 'neural', amount: 10 });
assert.ok(pre.findBuff('neuralBurst'));
const n0 = h.hooksOf('damaged').length;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
const splash = elemOf(h, u, n0).filter((c) => c.target === side);
assert.equal(splash.length, 1, 'T1\'s splash once around it');
approx(splash[0].amount, u.s.atk * 0.2, '20 % of her (S3) ATK');
const c1 = cagesOf(h).find((c) => c.tileR === 10 && c.tileC === 7);
assert.ok(c1 && pre.blockedBy === c1, 'a 牢笼 for the bursting one');
// a unit on the tile: none
const onAlly = h.spawn('enemy_dummy', { pos: [11, 6] });
onAlly.elem.neural = 999;
h.b.dealDamage(u, onAlly, { type: 'element', element: 'neural', amount: 10 });
assert.ok(!cagesOf(h).some((c) => c.tileR === 11 && c.tileC === 6), 'not on 角峰\'s tile');
// the skill ends: 30 s more
u.skill.end('test');
h.run(29.5);
if (pre.alive && pre.blockedBy === c1) assert.ok(c1.alive, 'still there at 29.5 s');
h.run(1);
assert.ok(!c1.alive, 'gone 30 s after the skill');
// her leaving withdraws them
u.skill.gainSp(999);
const late = h.spawn('enemy_dummy', { pos: [12, 7] });
assert.ok(h.runUntil(() => u.skill.active, 4));
late.elem.neural = 999;
h.b.dealDamage(u, late, { type: 'element', element: 'neural', amount: 10 });
assert.ok(cagesOf(h).length >= 1);
h.b.retreat(u);
h.step();
assert.equal(cagesOf(h).length, 0, 'withdrawn with her');
done(h);
});
+366
View File
@@ -0,0 +1,366 @@
// test/content/op_slent2.test.js — the 自选 operator kit of 淬羽赫默 (char_1031_slent2, 6★ 护佑者; kit
// server/sim/content/kits/ops/op-slent2.js) and of her summon 夜灯 (token_10029_slent2_protrb), fielded the production way
// (a DIY slot + its `diy` pick, simdata getDiy; the 夜灯 as the placed hand piece of her player) in every form: tiers 5 / 6,
// normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, BLS-X “难得清醒” or BLS-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_slent2.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 { diyPool, validateDiyPicks } from '../../shared/diy.js';
import { holdsUndying } from '../../server/sim/content/items/battle.js';
import { shelterOf, RHINE } from '../../server/sim/content/kits/ops/op-slent2.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 ID = 'char_1031_slent2';
const LAMP = 'token_10029_slent2_protrb';
const FORMS = BACKUPS.units[ID].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_slent2', Y = 'uniequip_003_slent2';
const S1 = 'skchr_slent2_1', S2 = 'skchr_slent2_2', S3 = 'skchr_slent2_3';
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
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' }) };
/** A synthetic 【莱茵生命】 ally (伊芙利特's charId; no skill) and a plain one. */
const RHINE_REC = chessRec({ id: 'test_rhine_a', charId: 'char_134_ifrit', profession: 'CASTER', skill: null, stats: { maxHp: 2000, def: 0, res: 0 } });
const PLAIN_REC = chessRec({ id: 'test_plain_a', charId: 'char_test_plain', profession: 'CASTER', skill: null, stats: { maxHp: 2000, def: 0, res: 0 } });
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, X, Y].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 淬羽赫默 as uid 1 at (10, 5) facing RIGHT, plus `others`; `lamp` = the 夜灯 piece's tile (uid 9) or null. */
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], lamp = null, lampFirst = false, seed = 5, flags = {} } = {}) {
const op = { uid: 1, diy: { slot: SLOT[tier], charId: ID, skillIndex: skill, uniEquipId: mod }, elite, row, col };
const piece = lamp ? [{ uid: 9, kind: 'token', tokenId: LAMP, ownerUid: 1, row: lamp[0], col: lamp[1] }] : [];
const h = makeBattle({
defs: { enemies: ENEMIES, chess: { test_rhine_a: RHINE_REC, test_plain_a: PLAIN_REC } }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999, ...flags }, hooks: ['damaged', 'skillStart', 'skillEnd', 'heal', 'death', 'deploy', 'fatal'], captureNoisy: true,
units: lampFirst ? [...piece, op, ...others] : [op, ...piece, ...others],
});
h.step();
return { h, u: h.unit(1), lampUnit: h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === LAMP) ?? null };
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const ally = (uid, row, col, id = 'test_plain_a') => ({ uid, chessId: id, row, col });
const protectOf = (a) => a.findBuff('protect')?.data?.value ?? 0;
test('淬羽赫默 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, y-6, 护佑者 (ranged arts, hits air, blocks 1; BLS-Y 2 targets), the data DEFAULT triggers, ground-targetable, 空构 bond, no 特质', () => {
assert.equal(OPERATOR_KITS[ID], KITS[ID]);
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], [ID, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
// BLS-X stage 3 gives her (a 莱茵生命 operator in her own range) max HP +10 % ×2
const selfHp = m?.uniEquipId === X && tier === 6 ? 1.2 : 1;
approx(u.s.maxHp, (form.stats.maxHp + (m?.attr.maxHp ?? 0)) * selfHp, `${label(f)}: max HP`);
assert.deepEqual([u.base.atk, u.base.def, u.base.res], [form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, form.stats.res], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 护佑者`);
assert.equal(u.profile.maxTargets, mod === Y && elite ? 2 : 1, `${label(f)}: targets`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-6`);
assert.equal(u.skill.rule, 'DEFAULT', `${label(f)}: trigger`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
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], [1445, 387, 1765, 440]);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr], [{ maxHp: 100, atk: 45 }, { maxHp: 150, atk: 70 }, { maxHp: 150, atk: 40 }, { maxHp: 200, atk: 62 }]);
});
test('a 自选 pick: 淬羽赫默 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(ID));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(ID), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: ID, skillIndex: 2, uniEquipId: Y } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: ID, skillIndex: 2, uniEquipId: Y } } });
});
test('Trait: arts attacks on enemies (BLS-Y: two targets, flyers too); while a skill runs every attack heals the most injured ally of her range for heal_scale × ATK (0.75; BLS-X 1.0; BLS-Y two allies) instead', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X], [6, true, Y]]) {
const tb = (elite ? modOf(tier, mod).traitOverride : formOf(tier, elite).trait).bb;
const { h, u } = field({ tier, elite, mod, skill: 0, others: [ally(2, 11, 6), ally(3, 9, 6), ally(4, 10, 6)] });
const e1 = h.spawn('enemy_dummy', { pos: [10, 7] }), e2 = h.spawn('enemy_fly', { pos: [11, 7] });
h.run(4);
assert.equal(u.skill.activations, 0, 'no SP yet');
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
assert.ok(hits.length >= 2 && hits.every((c) => c.type === 'arts'), `${tier}${mod}: arts hits`);
assert.equal(new Set(hits.map((c) => c.target)).size, mod === Y ? 2 : 1, `${tier}${mod}: targets`);
if (mod === Y) assert.ok(hits.some((c) => c.target === e2), 'the flyer too');
void e1;
const [a, b, c] = [h.unit(2), h.unit(3), h.unit(4)];
a.hp = a.s.maxHp * 0.3; b.hp = b.s.maxHp * 0.5; c.hp = c.s.maxHp * 0.9;
const n0 = h.hooksOf('heal').length;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast with an enemy in range');
assert.ok(h.runUntil(() => h.hooksOf('heal').slice(n0).some((x) => x.source === u && !x.opts?.regen), 3), 'heals');
const heals = h.hooksOf('heal').slice(n0).filter((x) => x.source === u && !x.opts?.regen);
const first = heals.filter((x) => Math.abs(x.t - heals[0].t) < 1e-9);
assert.deepEqual(first.map((x) => x.target.uid).sort(), (mod === Y ? [2, 3] : [2]).sort(), `${tier}${mod}: the most injured`);
for (const x of first) approx(x.amount, u.s.atk * tb.heal_scale, `${tier}${mod}: ${tb.heal_scale * 100} % ATK`);
const atk0 = h.hooksOf('damaged').filter((x) => x.source === u && x.dmg?.isAttack && x.t > heals[0].t - 1e-9);
assert.equal(atk0.length, 0, 'no attack on enemies while the skill heals');
done(h);
}
});
test('S1 进取之心 (MANUAL, DEFAULT): ATK +35 % / +50 % for 25 s, SP 35 / 30 from 10; cast on an enemy in her range, not on an injured ally alone', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const { h, u } = field({ tier, elite, skill: 0, others: [ally(2, 10, 6)] });
assert.deepEqual([u.skill.spCost, sk.initSp, sk.duration, sk.bb.atk], [elite ? 30 : 35, 10, 25, elite ? 0.5 : 0.35]);
h.unit(2).hp = 100;
u.skill.gainSp(999);
h.run(3);
assert.equal(u.skill.activations, 0, 'an injured ally alone casts nothing (a 辅助, not a 医疗)');
h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.active, 3));
approx(u.skill.timeLeft, 25, 'duration', 0.01);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), 'ATK');
h.runUntil(() => !u.skill.active, 30);
approx(u.s.atk, u.base.atk, 'back');
done(h);
}
});
test('S2 俯瞰视界 + 夜灯: ASPD +20 / +30 for 12 s; the placed 夜灯 deploys at the battle start, leaves when the skill ends, comes back on its tile with the next cast and leaves when she does; the allies of its x-4 hold her T1 庇护 ×2.1 / ×2.4; untargetable, no attack', () => {
for (const [tier, elite, lampFirst] of [[5, false, false], [6, true, true]]) {
const sk = skillOf(tier, elite, S2);
const { h, u, lampUnit } = field({ tier, elite, skill: 1, lamp: [12, 8], lampFirst, others: [ally(2, 12, 9), ally(3, 9, 9)] });
assert.deepEqual([sk.duration, sk.bb.attack_speed, sk.bb.damage_resistance_scale], [12, elite ? 30 : 20, elite ? 2.4 : 2.1]);
const lamp = lampUnit;
assert.ok(lamp && lamp.alive, 'deployed with the board (the battle-start deploy of a placed skill summon)');
assert.ok(lamp.s.flags.untargetable && lamp.profile.noAttack && lamp.s.blockCnt === 0, 'untargetable, no attack, no block');
const a = h.unit(2), far = h.unit(3);
a.hp = a.s.maxHp * 0.5;
h.run(0.3);
// (12, 9) is next to the 夜灯 and outside her y-6: 20 % × 2.1 / 2.4; (9, 9) outside both
const t1 = formOf(tier, elite).talents[0].bb;
approx(protectOf(a), shelterOf(t1, a, sk.bb.damage_resistance_scale), 'the 夜灯\'s 庇护', 1e-9);
approx(protectOf(a), 0.2 * sk.bb.damage_resistance_scale, '50 % HP: 20 % × scale', 1e-9);
assert.equal(protectOf(far), 0, 'outside');
h.spawn('enemy_dummy', { pos: [10, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
approx(u.s.aspd, 100 + sk.bb.attack_speed, 'ASPD');
assert.ok(lamp.alive, 'the start 夜灯 is this skill\'s drone');
assert.ok(h.runUntil(() => !u.skill.active, 13));
h.step();
assert.ok(!lamp.alive, 'withdrawn at the skill end');
h.run(0.3);
assert.equal(protectOf(a), 0, 'its 庇护 is gone');
// next cast (after its 5 s redeploy time): back on its tile
h.run(6);
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 4));
h.step();
assert.ok(lamp.alive && lamp.tileR === 12 && lamp.tileC === 8, 'back on its tile');
h.b.retreat(u);
h.step();
assert.ok(!lamp.alive, 'withdrawn when she leaves');
h.run(8);
assert.ok(!lamp.alive, 'not back without her skill');
done(h);
}
});
test('S2 without a placed 夜灯: nothing deploys; S1 / S3 picks never field the piece', () => {
const { h, u } = field({ skill: 1 });
h.spawn('enemy_dummy', { pos: [10, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
assert.equal(h.b.allyUnits.filter((a) => a.defId === LAMP).length, 0);
done(h);
for (const skill of [0, 2]) {
const r = field({ skill, lamp: [12, 8] });
r.h.run(2);
assert.ok(!r.lampUnit.alive, `skill ${skill}: no 夜灯`);
done(r.h);
}
});
test('T1 无声砥柱: every ally of her range (herself too) holds 庇护 base × (1 + 2 % per 1 % HP lost, at most 70 steps): 10 % → 20 % at half HP → 24 % at ≤ 30 %; it cuts physical and arts damage, not true; gone once the ally leaves her range; S3 ×1.5 / ×1.8', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X]]) {
const { h, u } = field({ tier, elite, mod, skill: 2, others: [ally(2, 10, 6), ally(3, 10, 9)] });
const a = h.unit(2), out = h.unit(3);
const t1 = formOf(tier, elite).talents[0].bb;
assert.deepEqual(t1, { damage_resistance_base: 0.1, resistance_scale: 0.02, hp_ratio: 0.01, min_hp_ratio: 0.3 });
h.run(0.2);
approx(protectOf(a), 0.1, 'full HP: 10 %', 1e-9);
approx(protectOf(u), 0.1, 'herself', 1e-9);
assert.equal(protectOf(out), 0, '(10, 9) is outside her y-6');
for (const [ratio, want] of [[0.5, 0.2], [0.555, 0.1 * (1 + 0.02 * 44)], [0.3, 0.24], [0.1, 0.24]]) {
a.hp = a.s.maxHp * ratio;
h.run(0.25);
approx(protectOf(a), want, `HP ${ratio}`, 1e-9);
}
// the cut: physical and arts ×(1 − 24 %), true untouched
const e = h.spawn('enemy_dummy', { pos: [11, 9] });
for (const type of ['phys', 'arts', 'true']) {
const hp0 = a.hp;
h.b.dealDamage(e, a, { amount: 100, type, canDodge: false });
approx(hp0 - a.hp, type === 'true' ? 100 : 76, `${type}`, 1e-6);
a.hp = a.s.maxHp * 0.1;
}
// S3: ×talent_scale
const sk3 = skillOf(tier, elite, S3);
h.spawn('enemy_dummy', { pos: [10, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
a.hp = a.s.maxHp * 0.1;
h.run(0.25);
approx(protectOf(a), 0.24 * sk3.bb.talent_scale, `S3 ×${sk3.bb.talent_scale}`, 1e-9);
// leaving the range: gone within one refresh
assert.ok(h.b.relocate(a, 12, 9));
h.run(0.3);
assert.equal(protectOf(a), 0, 'gone outside her range');
done(h);
}
});
test('T2 丰润羽翼: an ally of her range below 50 % HP gets 生命回复速度 5 % of her ATK (hpRegen, from 1 s after; 禁疗 does not stop it), a 【莱茵生命】 operator twice; none at ≥ 50 % or outside; BLS-X stage 3: 7 % and max HP +10 % (×2 莱茵生命) in her range; stage 1: neither', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, X], [6, true, X]]) {
const x3 = mod === X && tier === 6;
const { h, u } = field({ tier, elite, mod, skill: 0, others: [ally(2, 10, 6), ally(3, 11, 6, 'test_rhine_a'), ally(4, 9, 6)] });
const a = h.unit(2), r = h.unit(3), full = h.unit(4);
assert.ok(RHINE.has(r.def.charId) && !RHINE.has(a.def.charId));
const ratio = x3 ? 0.07 : 0.05;
assert.equal(formOf(tier, elite).talents[1].bb.atk_to_hp_recovery_ratio, 0.05);
approx(a.s.maxHp, 2000 * (x3 ? 1.1 : 1), 'max HP');
approx(r.s.maxHp, 2000 * (x3 ? 1.2 : 1), '莱茵生命 max HP');
h.b.addBuff(a, { key: 'test:healFree', flags: { noHeal: true, healFree: true } });
a.hp = a.s.maxHp * 0.4; r.hp = r.s.maxHp * 0.4;
h.run(0.95);
assert.equal(a.s.hpRegen, 0, 'not before 1 s');
h.run(0.2);
approx(a.s.hpRegen, u.s.atk * ratio, `${ratio * 100} % ATK`);
approx(r.s.hpRegen, u.s.atk * ratio * 2, '莱茵生命 ×2');
assert.equal(full.s.hpRegen, 0, 'full HP: none');
const hp0 = a.hp;
h.run(2);
approx(a.hp - hp0, u.s.atk * ratio * 2, '2 s of regeneration through 禁疗', 0.02);
a.hp = a.s.maxHp * 0.6;
h.run(0.15);
assert.equal(a.s.hpRegen, 0, 'stops at ≥ 50 %');
h.b.relocate(r, 10, 9);
h.run(0.15);
assert.equal(r.s.hpRegen, 0, 'stops outside her range');
done(h);
}
});
test('BLS-Y stage 3 (丰润羽翼): a hit that would leave an operator of her range below 50 % meets a 屏障 of 50 % of her max HP first (莱茵生命 ×2) — once per deployment of hers; entering her range below 50 % gives it too; stage 1 / no module: none', () => {
for (const [tier, elite, mod] of [[6, true, Y], [5, true, Y], [6, true, null]]) {
const on = mod === Y && tier === 6;
const { h, u } = field({ tier, elite, mod, skill: 0, others: [ally(2, 10, 6), ally(3, 11, 6, 'test_rhine_a'), ally(4, 10, 9)] });
const a = h.unit(2), r = h.unit(3), out = h.unit(4);
const e = h.spawn('enemy_dummy', { pos: [11, 9] });
h.run(0.2);
const cap = u.s.maxHp * 0.5;
// 2000 HP: 900 leaves 1100 (55 %): no barrier; then 200 would leave 900 (45 %)
h.b.dealDamage(e, a, { amount: 900, type: 'true' });
assert.equal(a.hp, 1100, 'above 50 %: no barrier');
h.b.dealDamage(e, a, { amount: 200, type: 'true' });
if (on) {
assert.equal(a.hp, 1100, 'the barrier took the hit');
approx(a.findBuff(`slent2:barrier#${u.id}`)?.shield ?? 0, cap - 200, 'what is left of it');
} else assert.equal(a.hp, 900, 'no barrier');
// once: a later drop gives none
a.hp = 1100;
for (const b of a.buffs.filter((x) => x.key.startsWith('slent2:barrier'))) h.b.removeBuff(a, b);
h.b.dealDamage(e, a, { amount: 200, type: 'true' });
assert.equal(a.hp, 900, 'once per deployment');
h.b.dealDamage(e, r, { amount: 1200, type: 'true' });
if (on) approx(r.hp, 2000 - Math.max(0, 1200 - 2 * cap), '莱茵生命: twice the barrier');
// entering her range below 50 %
out.hp = 500;
h.b.relocate(out, 11, 5);
h.run(0.2);
assert.equal(!!out.findBuff(`slent2:barrier#${u.id}`), on, 'on entering below 50 %');
// a new deployment of hers clears the marks
if (on) {
h.b.retreat(u);
h.b.redeploy(u);
h.run(0.2);
a.hp = 1100;
h.b.dealDamage(e, a, { amount: 200, type: 'true' });
assert.equal(a.hp, 1100, 'again after her redeploy');
}
done(h);
}
});
test('S3 无畏者协议 (60 s, at most 2 casts per battle): ATK +20 % / +30 %; the first operator of her range taking lethal damage keeps 1 HP and holds 不死 for 6 / 7 s (outlasting her) — once per cast, not a summon, not one outside her range, not one already holding 不死; no third cast', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const { h, u } = field({ tier, elite, skill: 2, others: [ally(2, 10, 6), ally(3, 11, 6), ally(4, 10, 9)] });
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.grave_duration, sk.bb.skill_max_trigger_time, u.skill.spCost, sk.initSp], [60, elite ? 0.3 : 0.2, elite ? 7 : 6, 2, elite ? 42 : 46, 20]);
const a = h.unit(2), b = h.unit(3), out = h.unit(4);
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), 'ATK');
h.b.dealDamage(e, out, { amount: 1e6, type: 'true' });
assert.ok(!out.alive, 'outside her range: no 不死');
h.b.dealDamage(e, a, { amount: 1e6, type: 'true' });
assert.ok(a.alive && a.hp === 1, 'kept at 1 HP');
assert.ok(a.findBuff('slent2:grave') && holdsUndying(h.b, a), '不死 (holdsUndying sees it)');
approx(a.findBuff('slent2:grave').timeLeft, sk.bb.grave_duration, 'duration', 0.01);
h.b.retreat(u); // the 不死 outlasts her
h.run(sk.bb.grave_duration - 0.5);
h.b.dealDamage(e, a, { amount: 1e6, type: 'true' });
assert.ok(a.alive, 'held during the window');
h.b.dealDamage(e, b, { amount: 1e6, type: 'true' });
assert.ok(!b.alive, 'once per cast');
h.run(1);
h.b.dealDamage(e, a, { amount: 1e6, type: 'true' });
assert.ok(!a.alive, 'the window is over');
done(h);
}
// two casts per battle, then no SP
const { h, u } = field({ skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 7] });
for (let i = 1; i <= 2; i++) {
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === i, 5), `cast ${i}`);
h.runUntil(() => !u.skill.active, 61);
}
h.run(100);
assert.equal(u.skill.activations, 2, 'no third cast');
assert.equal(u.skill.charges, 0);
assert.equal(u.skill.gainSp(50), 0, 'no SP after the last use');
done(h);
});
test('S3: a summon is no 干员 (no 不死); a lethal hit on an operator already holding 不死 leaves the cast\'s 不死 for the next one', () => {
const { h, u, lampUnit } = field({ skill: 2, others: [ally(2, 10, 6), ally(3, 11, 6)] });
void lampUnit;
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
const a = h.unit(2), b = h.unit(3);
// a summon of her range (a 夜灯 spawned for the test) takes no 不死
const tok = h.b.spawnToken(u, LAMP, 11, 5, { anySource: true });
assert.ok(tok && tok.alive && u.rangeKeySet.has(11 * 21 + 5));
h.b.dealDamage(e, tok, { amount: 1e6, type: 'true' });
assert.ok(!tok.alive && !tok.findBuff('slent2:grave'), 'a summon is no 干员');
h.b.addBuff(a, { key: 'test:undying', flags: { undying: true }, duration: 5 });
h.b.on('fatal', (c) => { if (c.unit === a && a.findBuff('test:undying')) c.prevented = true; }, { priority: -99 });
h.b.dealDamage(e, a, { amount: 1e6, type: 'true' });
assert.ok(a.alive && !a.findBuff('slent2:grave'), 'its own 不死 held it: the cast keeps its charge');
h.b.dealDamage(e, b, { amount: 1e6, type: 'true' });
assert.ok(b.alive && b.findBuff('slent2:grave'), 'the next operator gets it');
done(h);
});
+353 -111
View File
@@ -5217,8 +5217,8 @@
"snaps": "a3cff5cf d0583459 7816abc1 39def2d7 5b7f0286 c110a75a 695623ec 79136be7 a4d0b9a8 c2fa26f5 89fd1b16 5b47f0ed a212ac82 437033f1 c8d1ff58 534daf69 0c5a4ec8 caa8471f 0b73655b 49a2eb59 1a30387d 9158dadd 38cd69de"
},
"diy-122": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=radian/S1/002_radian 6_diy2_b=radian/S2/002_radian 6_diy1_b=radian/S3/002_radian 6_diy2_b=radian/S1/003_radian 6_diy1_b=radian/S2/003_radian 6_diy2_b=radian/S3/003_radian 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"about": "6_diy1_b=radian/S1/002_radian 6_diy2_b=radian/S2/002_radian 6_diy1_b=radian/S3/002_radian 6_diy2_b=radian/S1/003_radian 6_diy1_b=radian/S2/003_radian 6_diy2_b=radian/S3/003_radian 5_diy1_a=slent2/S1 5_diy2_a=slent2/S2 5_diy1_a=slent2/S3 5_diy2_b=slent2/S1 5_diy1_b=slent2/S2 5_diy2_b=slent2/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_multi_hard",
@@ -5234,113 +5234,113 @@
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":290050,"bossDmg":0,"heal":0,"deaths":18,"layerGains":{}}},
"players": {"p1":{"killed":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":289224,"bossDmg":0,"heal":35390,"deaths":16,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,138,0,152,6384,0,4,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,136,0,648,8724,0,9,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,140,0,7110,7692,0,18,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,133,0,7740,10002,0,15,0],
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_radian",1158,1,4,0,33369,0,0,131,4],
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_radian",1158,1,5,0,40275,0,0,127,4],
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_radian",1158,1,11,0,54384,0,0,129,3],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_radian",993,1,13,0,37218,0,0,135,4],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_radian",993,1,6,0,40898,0,0,122,4],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_radian",993,1,14,0,59223,0,0,132,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,151,0,882,3813,0,7,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,143,0,8152,4530,0,23,1],
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_radian",1158,1,4,0,33165,0,0,131,4],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_radian",1158,1,5,0,40835,0,0,129,4],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_radian",1158,1,11,0,57174,0,0,134,3],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_radian",993,1,13,0,38776,0,0,136,4],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_radian",993,1,6,0,41560,0,0,124,4],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_radian",993,1,14,0,59121,0,0,132,3],
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,148,0,2554,10770,5502,23,1],
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,146,0,4296,4873,538,19,0],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,139,0,4528,13518,9770,58,1],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,138,0,4092,5669,374,17,0],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,134,0,2024,5507,212,9,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,131,0,1100,20734,18994,43,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1010_demon":[54,0,53,18271,131927],"enemy_1422_lrsldr_2":[39,0,39,2889,92931],"enemy_1425_lrcmra_2":[30,0,30,19985,65192]},
"hooks": [1,151,6900,901,901,901,901,0,18,140,23,23,0,28074,220,29,123,122,1,18,0,0,0],
"events": {"atk":901,"deploy":25,"die":18,"dmg":901,"engage":12,"fx":8,"leak":122,"skill":46,"spawn":138,"status":236},
"fx": {"shield":8},
"snaps": "a5a3cc35 894a57a2 4b9f5176 6f48baf1 d88ae70f 473a84f3 74691a8e c52ef874 00871d23 c05d950a 33730f93 855c888e 8745c85a 2ff3981d ee3f55ee b28e36be 40d4d1ab 8151b1a7 10d62df3 1d5b7380 425c900d 412ab4f6 ce1f9af0"
"enemies": {"enemy_1010_demon":[54,0,53,35416,114473],"enemy_1422_lrsldr_2":[39,0,39,18914,105487],"enemy_1425_lrcmra_2":[30,0,30,6740,69264]},
"hooks": [1,148,6900,1058,1058,970,970,2601,16,138,25,25,0,28518,226,16,123,122,1,17,0,0,0],
"events": {"atk":1058,"deploy":22,"die":16,"dmg":970,"engage":12,"fx":9,"heal":87,"leak":122,"skill":50,"spawn":138,"status":250},
"fx": {"shield":8,"undying":1},
"snaps": "19191fcf 1d0dba78 f470eaef c5134454 b45ee383 30798725 0999838f 9bdfec73 df515beb 6aade2dd 304f007b 9e0478d7 c315937c b6cf43a4 782d51f9 70b71cba d841b1b3 53bdfe56 c955e66b 1fcca0e4 75f58321 0ad54f22 f0c2122a"
},
"diy-123": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"about": "5_diy1_b=slent2/S1/002_slent2 5_diy2_b=slent2/S2/002_slent2 5_diy1_b=slent2/S3/002_slent2 5_diy2_b=slent2/S1/003_slent2 5_diy1_b=slent2/S2/003_slent2 5_diy2_b=slent2/S3/003_slent2 6_diy1_a=slent2/S1 6_diy2_a=slent2/S2 6_diy1_a=slent2/S3 6_diy2_b=slent2/S1 6_diy1_b=slent2/S2 6_diy2_b=slent2/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_single_normal",
"round": 12,
"round": 13,
"seed": 983089170,
"waveId": "act1autochess_h05",
"time": 217.53,
"ticks": 6526,
"reason": "cleared",
"killed": 16,
"total": 90,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 13,
"total": 36,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"rngDraws": 11,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":16,"total":90,"perfect":false,"leaks":74,"leaksUncounted":0,"coins":0,"dmg":305235,"bossDmg":0,"heal":0,"deaths":16,"layerGains":{}}},
"players": {"p1":{"killed":13,"total":36,"perfect":false,"leaks":23,"leaksUncounted":0,"coins":0,"dmg":138812,"bossDmg":0,"heal":102234,"deaths":20,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,112,0,611,7812,0,19,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,99,0,6445,6742,0,26,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,89,4,25438,3058,0,24,4],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,79,3,83870,3808,0,25,4],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,84,1,37691,5276,0,34,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,71,6,46607,6822,0,31,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,5,2,26403,0,0,95,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,8,0,52045,0,0,158,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,6,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,9,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,105,0,6636,1864,0,28,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,103,0,19488,1131,0,70,2]
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_slent2",0,0,59,1,8236,4235,15683,47,2],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_slent2",391.24,1,56,0,19594,11003,9671,70,3],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_slent2",0,0,58,0,6111,6836,17807,50,1],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_slent2",0,0,54,0,22272,8564,16445,58,2],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_slent2",0,0,1,0,20448,1915,1080,28,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_slent2",0,0,3,0,8736,2420,15670,46,1],
["p1",7,"op","chess_char_6_diy1_a",0,null,907.65,1,57,0,8127,2463,7306,49,2],
["p1",8,"op","chess_char_6_diy2_a",1,null,727.87,1,55,0,15635,5605,5395,65,2],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,52,1,4925,4606,271,13,0],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,51,0,4576,5753,458,11,0],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,2,0,11616,1765,1089,36,2],
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,53,0,8536,84599,11360,57,1]
],
"enemies": {"enemy_1229_darmy":[27,5,22,5381,47975],"enemy_1229_darmy_2":[51,10,41,14921,150809],"enemy_9007_acelem":[12,1,11,16212,106450]},
"hooks": [1,112,6526,573,573,842,898,0,32,106,22,21,0,22719,98,33,90,74,1,37,0,2,0],
"events": {"atk":573,"deploy":22,"die":32,"dmg":898,"engage":10,"fx":106,"leak":74,"skill":43,"spawn":102,"status":238},
"fx": {"aoe":24,"beam":12,"burst":2,"dp":15,"slash":53},
"snaps": "06c160ad 60856643 f2759344 c1828e1a 2d800fed b15a5a70 1dde37fe a2c4da33 062734bc b8121d72 29030c51 f7b6b38b 6ba27ec2 429425fd 35458d83 075dbff4 e90e78aa 235ce236 5dbe07be 649356b2 6da48fb9 024e3c4c"
"enemies": {"enemy_1005_yokai_2":[12,12,0,136,2893],"enemy_1005_yokai_3":[12,0,11,70354,50146],"enemy_9009_acfort":[12,1,11,69272,85774]},
"hooks": [1,59,6900,913,913,744,744,1250,33,55,18,18,0,22609,0,0,36,22,1,32,0,0,0],
"events": {"atk":983,"deploy":23,"die":33,"dmg":744,"engage":12,"fx":46,"heal":244,"leak":22,"skill":36,"spawn":48,"status":26},
"fx": {"beam":44,"undying":2},
"snaps": "b16e798f ee059fec 8ad3bfc7 3d6d8ebe 0cef81b3 3e6d32d8 da3d71eb f93663d5 13f1bf4c 1427fcee 5d955be0 61d23a2a fc44ffff 6bfa751e 79d902a9 b4e0eb21 b334bdd1 ab3a70a0 edee414e 52fc350a adb3ce09 488b6129 6dbe5135"
},
"diy-124": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"about": "6_diy1_b=slent2/S1/002_slent2 6_diy2_b=slent2/S2/002_slent2 6_diy1_b=slent2/S3/002_slent2 6_diy2_b=slent2/S1/003_slent2 6_diy1_b=slent2/S2/003_slent2 6_diy2_b=slent2/S3/003_slent2 5_diy1_a=haruka/S1 5_diy2_a=haruka/S2 5_diy1_a=haruka/S3 5_diy2_b=haruka/S1 5_diy1_b=haruka/S2 5_diy2_b=haruka/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_multi_abyss",
"round": 12,
"round": 13,
"seed": 3637921807,
"waveId": "act1autochess_h05",
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 19,
"total": 114,
"killed": 3,
"total": 147,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1060,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":19,"total":114,"perfect":false,"leaks":95,"leaksUncounted":0,"coins":0,"dmg":875741,"bossDmg":0,"heal":90677,"deaths":7,"layerGains":{}}},
"players": {"p1":{"killed":3,"total":147,"perfect":false,"leaks":144,"leaksUncounted":0,"coins":0,"dmg":366819,"bossDmg":0,"heal":143980,"deaths":1,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,1865,1,134,2,32671,3730,0,52,2],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,136,5,86795,7181,0,59,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,135,1,35817,31242,0,195,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",789.36,1,122,4,60783,31411,0,213,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,8,2,32124,0,0,111,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,12,0,55163,0,0,120,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,14,2,80444,0,0,49,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,4,3,402021,0,0,84,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,6,0,0,0,39232,76,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1211.94,1,9,0,0,30207,51445,76,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,13,0,44372,0,0,158,3],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,15,0,45551,0,0,130,2],
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_slent2",2298,1,6,0,20859,0,24292,124,4],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_slent2",2298,1,11,1,29640,0,10044,129,5],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_slent2",2298,1,14,1,17854,0,13242,101,2],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_slent2",1965,1,4,0,29367,0,7597,81,4],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_slent2",1965,1,5,0,48041,0,4541,116,5],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_slent2",2358,1,7,0,34086,19467,21058,110,2],
["p1",7,"op","chess_char_5_diy1_a",0,null,1556.5,1,12,0,17472,0,9369,109,5],
["p1",8,"op","chess_char_5_diy2_a",1,null,1556.5,1,15,0,23546,0,9318,52,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1556.5,1,8,0,30895,53685,12140,119,3],
["p1",10,"op","chess_char_5_diy2_b",0,"none",1900.8,1,10,1,19822,27403,11008,112,5],
["p1",11,"op","chess_char_5_diy1_b",1,"none",1900.8,1,85,0,46449,19336,4708,56,2],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1900.8,1,9,0,48789,25991,16664,118,3],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_10124_uashld_2":[12,0,12,1442,109491],"enemy_1043_zomsbr":[72,19,53,46440,483740],"enemy_1061_zomshd":[30,0,29,55889,282511]},
"hooks": [1,136,6900,1658,1658,2344,2424,42351,26,120,23,23,0,36633,7376,27,114,94,1,26,0,0,0],
"events": {"atk":1877,"deploy":19,"die":26,"dmg":2416,"engage":10,"fx":48,"heal":262,"leak":94,"skill":53,"spawn":129,"status":840},
"fx": {"aoe":20,"displace":4,"dodge":16,"spGain":8},
"snaps": "1eb6ea3d 97f3ff30 4261b18e c01456c9 0b5c62d2 fab04043 177e6583 b52b5b2e be9178b3 8b79e9c3 1922dfdf a92f76da 8d4df36e ef41dec7 ad0b61b1 661c90f4 e2890b0a 59077292 95d5bd04 0243fc56 ac750351 03a10c61 9ba3299d"
"enemies": {"enemy_1010_demon_2":[36,0,35,45739,83976],"enemy_1071_dftman":[51,2,49,12571,78341],"enemy_1072_dlancer":[60,1,57,87571,204503]},
"hooks": [1,163,6900,1507,1507,1705,1705,695,4,145,42,38,0,42811,57,63,147,141,1,5,0,0,0],
"events": {"atk":1709,"deploy":13,"die":4,"dmg":1703,"engage":12,"fx":683,"heal":493,"leak":141,"skill":80,"spawn":162,"status":660},
"fx": {"aoe":80,"heal":264,"levitate":57,"shield":281,"undying":1},
"snaps": "ab31c401 593ca698 3f161f46 c7d7b74b 6fac8158 239a95a3 d66a3e6d 2cbd9073 b6e2f25c 4edeb819 2dab0390 f67eb664 86218b39 68bd30dc 03ffc17d ad9bf4a1 aa51e149 0d1cbd08 cbf073fb 4185d001 dd7968d9 3f78af44 ee57d9a2"
},
"diy-125": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
"about": "5_diy1_b=haruka/S1/002_haruka 5_diy2_b=haruka/S2/002_haruka 5_diy1_b=haruka/S3/002_haruka 6_diy1_a=haruka/S1 6_diy2_a=haruka/S2 6_diy1_a=haruka/S3 6_diy2_b=haruka/S1 6_diy1_b=haruka/S2 6_diy2_b=haruka/S3 6_diy1_b=haruka/S1/002_haruka 6_diy2_b=haruka/S2/002_haruka 6_diy1_b=haruka/S3/002_haruka",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m04",
@@ -5348,71 +5348,313 @@
"round": 13,
"seed": 1997254780,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 84,
"time": 203.63,
"ticks": 6109,
"reason": "cleared",
"killed": 123,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 343,
"rngDraws": 90,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":84,"total":123,"perfect":false,"leaks":39,"leaksUncounted":0,"coins":0,"dmg":357927,"bossDmg":0,"heal":66543,"deaths":16,"layerGains":{}}},
"players": {"p1":{"killed":123,"total":123,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":629853,"bossDmg":0,"heal":128695,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,1893.31,1,123,2,5031,3315,0,33,1],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",3431,1,151,5,13919,3431,0,21,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,126.12,1,127,12,20826,2932,0,21,5],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1887.63,1,150,7,32985,6563,0,15,3],
["p1",5,"op","chess_char_6_diy1_a",2,null,2498,1,94,17,82020,30131,0,83,4],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",3415.68,1,122,22,98741,12217,0,83,2],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,142,0,3495,1272,0,4,1],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,146,10,48097,1643,0,73,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,136,1,10025,4116,0,8,0],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,7,4,42788,1819,0,23,1],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,88,0,0,10841,20168,35,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1186.2,1,84,0,0,21228,46375,67,3],
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_haruka",1858,1,4,0,0,0,3164,9,1],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_haruka",1858,1,5,1,7912,0,1590,6,1],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_haruka",1858,1,15,17,73075,14554,9713,114,2],
["p1",4,"op","chess_char_6_diy1_a",0,null,1415,1,6,0,2866,0,6513,48,3],
["p1",5,"op","chess_char_6_diy2_a",1,null,1415,1,8,8,35170,0,11720,97,2],
["p1",6,"op","chess_char_6_diy1_a",2,null,1415,1,10,16,47405,14408,9923,130,2],
["p1",7,"op","chess_char_6_diy2_b",0,"none",1728,1,12,7,21082,4346,8175,124,5],
["p1",8,"op","chess_char_6_diy1_b",1,"none",1728,1,14,16,97813,10827,13434,80,2],
["p1",9,"op","chess_char_6_diy2_b",2,"none",1728,1,7,1,17016,5796,13003,68,2],
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_haruka",1938,1,9,3,19112,12473,10312,90,4],
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_haruka",1938,1,11,30,232380,38855,27752,125,2],
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_haruka",1938,1,13,20,76022,27435,13396,113,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1019_jshoot_2":[57,47,9,26899,136695],"enemy_1299_ymkilr_2":[27,14,13,42870,161687],"enemy_1422_lrsldr_2":[39,23,16,8181,142594]},
"hooks": [1,151,6900,1072,1072,2676,2599,203,100,138,24,23,0,26014,0,97,123,38,1,100,0,0,77],
"events": {"atk":1124,"deploy":25,"die":100,"dmg":2599,"engage":10,"fx":179,"heal":178,"leak":38,"skill":47,"spawn":138,"status":16},
"fx": {"aoe":33,"dodge":77,"dp":27,"slash":40,"spGain":2},
"snaps": "21dd8d16 cabbff5b caf3b53e 7c015a2c 6747114f f22456ce ed18dbd1 652b0a6d 11ae3137 c8c632f8 f25abdbc 4667ab53 fab2e5d0 ed9faf27 fcec0ce7 e591aee0 5f8d1706 17d2d702 aeab44ae 4e764aa3 2c433a83 5127ab47 837a8334"
"enemies": {"enemy_1019_jshoot_2":[57,57,0,19648,147744],"enemy_1299_ymkilr_2":[27,27,0,75006,285120],"enemy_1422_lrsldr_2":[39,39,0,5622,205920]},
"hooks": [1,138,6109,1425,1425,2509,2509,1034,123,123,28,24,0,30459,106,119,123,0,1,123,0,0,0],
"events": {"atk":1700,"deploy":12,"die":123,"dmg":2509,"engage":10,"fx":1024,"heal":845,"skill":52,"spawn":138,"status":769},
"fx": {"aoe":324,"heal":291,"levitate":106,"shield":303},
"snaps": "37a19233 3498a973 d7fd62db d432a99e 817dc20a 14c36445 1e36dfc0 0214eca3 4a66caa3 d431f91a 76c9483a 90a33b46 64d1a3cc f5b92579 7f25eb9f c4e59fde 114d2732 e3f08694 1106788c d2ed4180 056d1923"
},
"diy-126": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
"about": "5_diy1_a=phatm2/S1 5_diy2_a=phatm2/S2 5_diy1_a=phatm2/S3 5_diy2_b=phatm2/S1 5_diy1_b=phatm2/S2 5_diy2_b=phatm2/S3 5_diy1_b=phatm2/S1/002_phatm2 5_diy2_b=phatm2/S2/002_phatm2 5_diy1_b=phatm2/S3/002_phatm2 6_diy1_a=phatm2/S1 6_diy2_a=phatm2/S2 6_diy1_a=phatm2/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m05",
"modeId": "mode_single_hard",
"round": 13,
"round": 12,
"seed": 356460561,
"waveId": "act1autochess_h05",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 100,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":100,"total":102,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":2039144,"bossDmg":0,"heal":0,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,936,1,10,2,133730,0,0,98,24],
["p1",2,"op","chess_char_5_diy2_a",1,null,936,1,11,3,176570,0,0,162,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,936,1,1,2,67780,0,0,51,2],
["p1",4,"op","chess_char_5_diy2_b",0,"none",1111,1,3,5,71994,0,0,52,13],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1111,1,5,7,93448,0,0,129,1],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1111,1,7,14,466653,0,0,135,3],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_phatm2",1236,1,9,9,150648,0,0,135,33],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_phatm2",1236,1,12,9,263202,0,0,172,1],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_phatm2",1236,1,2,4,86955,0,0,51,2],
["p1",10,"op","chess_char_6_diy1_a",0,null,936,1,4,0,26175,0,0,52,13],
["p1",11,"op","chess_char_6_diy2_a",1,null,936,1,6,8,152179,0,0,125,1],
["p1",12,"op","chess_char_6_diy1_a",2,null,936,1,8,18,349810,0,0,135,3],
["p1","~1","token","token_10055_phatm2_mndclv",null,null,3,0,28,0,0,0,0,0,null],
["p1","~2","token","token_10055_phatm2_mndclv",null,null,3,0,29,0,0,0,0,0,null],
["p1","~3","token","token_10055_phatm2_mndclv",null,null,0,0,30,0,0,16,0,0,null],
["p1","~4","token","token_10055_phatm2_mndclv",null,null,3,0,33,0,0,0,0,0,null],
["p1","~5","token","token_10055_phatm2_mndclv",null,null,3,0,34,0,0,0,0,0,null],
["p1","~6","token","token_10055_phatm2_mndclv",null,null,3,0,38,0,0,0,0,0,null],
["p1","~7","token","token_10055_phatm2_mndclv",null,null,3,0,39,0,0,0,0,0,null],
["p1","~8","token","token_10055_phatm2_mndclv",null,null,3,0,44,0,0,0,0,0,null],
["p1","~9","token","token_10055_phatm2_mndclv",null,null,3,0,46,0,0,0,0,0,null],
["p1","~10","token","token_10055_phatm2_mndclv",null,null,3,0,49,0,0,0,0,0,null],
["p1","~11","token","token_10055_phatm2_mndclv",null,null,0,0,56,0,0,3,0,0,null],
["p1","~12","token","token_10055_phatm2_mndclv",null,null,3,0,81,0,0,1,0,0,null],
["p1","~13","token","token_10055_phatm2_mndclv",null,null,3,0,82,0,0,0,0,0,null],
["p1","~14","token","token_10055_phatm2_mndclv",null,null,2,0,83,0,0,1,0,0,null],
["p1","~15","token","token_10055_phatm2_mndclv",null,null,0,0,85,0,0,15,0,0,null],
["p1","~16","token","token_10055_phatm2_mndclv",null,null,3,0,87,0,0,0,0,0,null],
["p1","~17","token","token_10055_phatm2_mndclv",null,null,0,0,102,0,0,6,0,0,null],
["p1","~18","token","token_10055_phatm2_mndclv",null,null,0,0,110,0,0,5,0,0,null],
["p1","~19","token","token_10055_phatm2_mndclv",null,null,3,0,127,0,0,0,0,0,null],
["p1","~20","token","token_10055_phatm2_mndclv",null,null,2,0,128,0,0,1,0,0,null],
["p1","~21","token","token_10055_phatm2_mndclv",null,null,1,0,129,0,0,7,0,0,null],
["p1","~22","token","token_10055_phatm2_mndclv",null,null,3,0,130,0,0,0,0,0,null]
],
"enemies": {"enemy_1166_dusbr_2":[45,45,0,37,273715],"enemy_1170_dushld_2":[27,27,0,11,335923],"enemy_9011_acrefr":[30,28,0,7,1484094]},
"hooks": [1,136,6900,1352,1352,3126,6459,0,105,122,97,93,0,18830,411,118,102,0,1,105,0,332,0],
"events": {"atk":1352,"deploy":34,"die":122,"dmg":6459,"engage":12,"fx":593,"skill":190,"spawn":136,"status":936},
"fx": {"burst":332,"palsy":239,"summon":22},
"snaps": "c4749bd0 699d0248 17649b18 7ab3fc70 09e3e1e3 6258b044 7238ac54 9f57607a 65ba9fc3 0b8e90f3 add042bf e94c1af2 fdf621ea 0e16b2df 721d914c d0aaa024 f828a7ac 7da502ae c52b962c 070c0db9 2a07d5b3 eca02286 05f89efe"
},
"diy-127": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=phatm2/S1 6_diy2_b=phatm2/S2 6_diy1_b=phatm2/S3 6_diy2_b=phatm2/S1/002_phatm2 6_diy1_b=phatm2/S2/002_phatm2 6_diy2_b=phatm2/S3/002_phatm2 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act1autochess_m06",
"modeId": "mode_multi_normal",
"round": 13,
"seed": 3010682921,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 10,
"killed": 30,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 170,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":10,"total":105,"perfect":false,"leaks":95,"leaksUncounted":0,"coins":0,"dmg":171811,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"players": {"p1":{"killed":30,"total":105,"perfect":false,"leaks":75,"leaksUncounted":0,"coins":0,"dmg":1853739,"bossDmg":0,"heal":0,"deaths":12,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,1865,1,128,2,23625,3730,0,51,2],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",2494,1,129,6,88477,4988,0,57,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,127,2,19834,9232,0,83,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,130,0,14467,11872,0,70,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,5,0,4933,0,0,31,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,6,0,20475,0,0,82,2]
["p1",1,"op","chess_char_5_diy1_a",2,null,7.11,1,3,0,4818,2121,0,144,3],
["p1",2,"op","chess_char_5_diy2_b",2,"none",2908,1,6,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,127,0,7994,10002,0,41,0],
["p1",5,"op","chess_char_6_diy1_b",0,"none",1111,1,1,8,375991,0,0,131,32],
["p1",6,"op","chess_char_6_diy2_b",1,"none",274.62,1,4,4,332693,836,0,164,1],
["p1",7,"op","chess_char_6_diy1_b",2,"none",1111,1,124,8,321763,1111,0,93,3],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_002_phatm2",1311,1,2,5,446608,0,0,131,32],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_002_phatm2",0,0,131,4,158830,2622,0,95,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_phatm2",474.62,1,135,1,193400,3458,0,53,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,118,0,3784,3813,0,30,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,136,0,7859,1510,0,152,2],
["p1","~1","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~2","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~3","token","token_10055_phatm2_mndclv",null,null,3,0,28,0,0,0,0,0,null],
["p1","~4","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~5","token","token_10055_phatm2_mndclv",null,null,3,0,32,0,0,0,0,0,null],
["p1","~6","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~7","token","token_10055_phatm2_mndclv",null,null,3,0,34,0,0,0,0,0,null],
["p1","~8","token","token_10055_phatm2_mndclv",null,null,3,0,38,0,0,0,0,0,null],
["p1","~9","token","token_10055_phatm2_mndclv",null,null,3,0,41,0,0,0,0,0,null],
["p1","~10","token","token_10055_phatm2_mndclv",null,null,3,0,75,0,0,0,0,0,null],
["p1","~11","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~12","token","token_10055_phatm2_mndclv",null,null,3,0,76,0,0,0,0,0,null],
["p1","~13","token","token_10055_phatm2_mndclv",null,null,3,0,77,0,0,0,0,0,null],
["p1","~14","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~15","token","token_10055_phatm2_mndclv",null,null,3,1,133,0,0,0,0,0,null],
["p1","~16","token","token_10055_phatm2_mndclv",null,null,3,1,134,0,0,0,0,0,null],
["p1","~17","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~18","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null]
],
"enemies": {"enemy_1165_duhond":[51,10,41,866,73785],"enemy_1174_duholy_2":[24,0,23,12955,102349],"enemy_1175_dushdo_2":[30,0,30,16001,67989]},
"hooks": [1,130,6900,441,441,2345,2417,0,30,124,6,21,0,10384,2389,33,105,94,1,30,0,0,3],
"events": {"atk":485,"deploy":25,"die":30,"dmg":2387,"engage":6,"fx":8,"leak":94,"skill":45,"spawn":111,"status":481},
"fx": {"displace":5,"dodge":3},
"snaps": "08186948 cba346f8 3113c1fa 3712ce41 c1534fdd 8a993ce5 c092176d 86d47d10 903c4f73 0bf3993b f31f3e91 67179f99 c5384e6c 41f8b47e 8d537e1e d1394a25 4f860256 61990651 bf5d7786 d729c8d1 3aa4f821 3e5745ec aaa11631"
"enemies": {"enemy_1116_liprr_2":[27,9,18,3738,280676],"enemy_1119_vofsd":[48,15,29,15202,794636],"enemy_1121_lifbos_2":[30,6,24,6534,778428]},
"hooks": [1,136,6900,1074,1074,1383,3796,1745,42,121,76,73,0,17719,311,60,105,71,1,42,0,251,0],
"events": {"atk":1074,"deploy":31,"die":50,"dmg":3796,"engage":10,"fx":486,"leak":71,"skill":149,"spawn":127,"status":752},
"fx": {"burst":251,"liberate":3,"palsy":222,"summon":10},
"snaps": "ff349734 43b3c2d1 5e09fdbf 588f8bb7 62eaf80f bf97ee30 1b8c2f7d 647f33c5 e595a119 78a82ab2 0afc7940 29efd966 5faf9bc5 4a9efa2e a6f21a05 1e94de9b a6059141 152adafe 8779d264 2b7ac375 c3fb645c 3dd7bb76 e51f73ed"
},
"diy-128": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_hard",
"round": 12,
"seed": 1370290236,
"waveId": "act1autochess_h05",
"time": 192,
"ticks": 5760,
"reason": "cleared",
"killed": 7,
"total": 93,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 18,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":7,"total":93,"perfect":false,"leaks":86,"leaksUncounted":0,"coins":0,"dmg":234674,"bossDmg":0,"heal":0,"deaths":16,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,84,1,14487,5208,0,52,1],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,76,1,13007,6742,0,41,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,114,1,14653,4587,0,18,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,110,1,73028,5712,0,27,2],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,117,1,8944,5276,0,13,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,73,2,51862,6822,0,33,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,0,14757,0,0,56,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,7,0,16360,0,0,56,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,9,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,11,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,13,0,13331,0,0,57,2],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,70,0,14245,2262,0,38,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_10065_ftzlc":[63,7,56,12751,169866],"enemy_10067_ftsjc":[30,0,30,23858,64809]},
"hooks": [1,117,5760,641,641,734,750,0,23,109,16,16,0,20287,95,37,93,86,1,26,0,0,8],
"events": {"atk":641,"deploy":22,"die":23,"dmg":750,"engage":10,"fx":83,"leak":86,"skill":32,"spawn":107,"status":196},
"fx": {"dodge":8,"dp":5,"explode":14,"ignite":32,"slash":24},
"snaps": "b735394e 44df6814 4ede3523 ceaca815 0990904d 0cf45d51 3f5f612a 3392f0f3 a2cb1d61 6c52af78 9415f3b8 49b70461 7f144b13 c9769b73 a22d4adb 6b2c276d 4b214962 73629044 b830e5dc 0cb0f98c"
},
"diy-129": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_single_normal",
"round": 12,
"seed": 208547970,
"waveId": "act1autochess_h05",
"time": 213.77,
"ticks": 6413,
"reason": "cleared",
"killed": 40,
"total": 159,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 260,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":40,"total":159,"perfect":false,"leaks":119,"leaksUncounted":0,"coins":0,"dmg":227420,"bossDmg":0,"heal":4309,"deaths":28,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,6,9,34156,1865,0,26,1],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,9,7,75183,2374,0,31,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,190,0,7213,9373,0,21,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",95.94,1,188,1,18424,10040,0,33,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,166,13,44376,2544,0,48,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",758.45,1,189,7,35386,2416,0,37,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,4,0,0,1372,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,114,3,11915,4113,0,5,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,8,0,0,2434,2643,7,1],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,103,0,0,5171,1666,3,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,1,0,0,1160,0,0,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,102,0,768,2860,0,2,1]
],
"enemies": {"enemy_1009_lurker":[72,18,54,2183,64312],"enemy_1019_jshoot_2":[51,13,38,16138,60642],"enemy_1404_msnip":[36,9,27,27401,102466]},
"hooks": [1,190,6413,485,485,717,754,15,68,187,8,22,0,8825,82,56,159,119,1,68,0,0,1],
"events": {"atk":560,"deploy":31,"die":68,"dmg":751,"engage":8,"fx":9,"heal":13,"leak":119,"skill":48,"spawn":171,"status":77},
"fx": {"aoe":3,"displace":3,"dodge":1,"spGain":2},
"snaps": "cb7c5d0f 22a665b0 b0241d3f 8234db69 6d6af92a 4df4630a fbb444f9 7e8e6ab5 d60faeb1 c543720a 31f9bc0d 2619ab64 9dc146d2 0df3dd1d 845dbd2b 7141ab9c aaad4844 c5a03dc3 284e9904 bb59254b 5385151c 09b91371"
},
"diy-130": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_multi_abyss",
"round": 13,
"seed": 2862651535,
"waveId": "act1autochess_h06",
"time": 220.7,
"ticks": 6621,
"reason": "cleared",
"killed": 1,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 369,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":1,"total":102,"perfect":false,"leaks":101,"leaksUncounted":0,"coins":0,"dmg":937924,"bossDmg":0,"heal":87780,"deaths":16,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,118,0,443,7812,0,7,0],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,121,0,1812,10293,0,15,0],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,114,0,13093,5419,0,11,1],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,113,0,37999,8915,0,11,2],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,108,0,91547,38701,0,80,4],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,107,0,159873,57383,0,81,4],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,0,16194,0,0,45,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,9,1,125710,0,0,156,2],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,13,0,31368,0,0,37,2],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,0,459886,0,0,85,3],
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,40530,44,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,10,0,0,0,47250,44,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1267_nhpbr_2":[27,0,27,2985,177195],"enemy_1270_nhstlk_2":[36,1,35,21217,245338],"enemy_9006_actoxi":[39,0,39,104320,515392]},
"hooks": [1,126,6621,804,804,1335,1327,183,17,118,24,23,0,28925,0,35,102,101,1,22,0,0,8],
"events": {"atk":921,"deploy":22,"die":17,"dmg":1327,"engage":10,"fx":66,"heal":181,"leak":101,"skill":47,"spawn":116,"status":36},
"fx": {"aoe":14,"dodge":8,"dp":3,"slash":24,"spGain":17},
"snaps": "991457ea e05dc464 b3371c0c 96324561 4e55a051 849b28ac 6aaff9a0 e19921e6 e0540cc5 9dda1ab1 0d299067 9b6f5ee2 e612af65 30447a83 843f3646 1d4d8894 581172e5 afeaf942 e3a403f5 27743746 e033f10e a3b5099b"
},
"diy-131": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m03",
"modeId": "mode_multi_funny",
"round": 13,
"seed": 1967364563,
"waveId": "act1autochess_h06",
"time": 213.2,
"ticks": 6396,
"reason": "cleared",
"killed": 34,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 287,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":34,"total":123,"perfect":false,"leaks":89,"leaksUncounted":0,"coins":0,"dmg":303954,"bossDmg":0,"heal":0,"deaths":18,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,115,2,12461,3730,0,44,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,94,11,80882,4988,0,58,2],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,148,5,20771,8078,0,95,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",273.68,1,147,5,28658,11598,0,87,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,6,7,64863,0,0,146,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,7,4,96319,0,0,156,3],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1006_shield_3":[21,0,21,13429,72237],"enemy_1043_zomsbr":[66,27,39,93,102526],"enemy_1402_tgshd_2":[36,7,29,14872,129191]},
"hooks": [1,148,6396,693,693,920,1011,11182,52,141,8,21,0,10798,12327,37,123,89,1,52,0,0,0],
"events": {"atk":764,"deploy":22,"die":52,"dmg":945,"engage":6,"fx":503,"leak":89,"skill":47,"spawn":132,"status":927},
"fx": {"displace":19,"dodge":5,"expose":479},
"snaps": "d456be72 24986da0 263291b2 6601c930 43a57b58 57e6c19a e9793217 f2307500 9bf48dd7 a1e1185f f757a9dd 9feca978 f1099997 885b8762 df7edeeb d75a4c78 dbb2b23e 3942d750 75b12534 b102163d 3fee0d49 966739a7"
}
}
}