feedback5 O22: 自选 kits for 淬羽赫默 (with her 夜灯), 遥 and 酒神 (with her 本能的召唤 and 迷狂牢笼)

Three owned-6★ picks get their operator kits (kits/ops/op-slent2.js, op-haruka.js, op-phatm2.js; OPERATOR_KIT_FILES):
every skill at rank 4 / 7, both talents, the trait and every module at stages 1 and 3, from data/backups.json, PRTS and the
client's battle data (charpacks + buff_template_data of the three operators).

- 淬羽赫默: 护佑者 (arts attacks, heals while a skill runs; BLS-X 100 %, BLS-Y 2 targets); 无声砥柱 庇护 10 % → 24 % by the
  lost HP (shared `protect`); 丰润羽翼 as 生命回复速度 (the atk_to_hp_recovery template PRTS documents so for 絮雨 / 琴柳), ×2
  for 莱茵生命; BLS-X Lv3 max HP +10 %, BLS-Y Lv3 the once-per-deployment barrier; S2's placed 夜灯 (T1 ×2.1 / ×2.4 around it,
  withdrawn at the skill's end or when she leaves); S3's 不死 for the first lethal hit in her range, 2 uses a battle.
- 遥: bubbles with each attack (or instead of one), 30 % 庇护 ([bonus], ×1.4 / ×1.7 in S3), breaking 0.5 s after an enemy's
  damage, the break heal (BLS-Y Lv3 +1 SP to the holder); S1 re-bubbles around, S2 heal → arts splash and endless from the
  2nd use, S3 y-8 and the levitation with its damage.
- 酒神: T1 神经损伤 on each hit + 1.3 splash; 堕梦 ASPD cut in 神经 bursts and 70 / 90 on enemy attacks (a burst interrupts
  the attack); RIT-X ×1.18 vs elite / leader; S1 two hits + 束缚 ×1.3 / ×1.5; S2 (SP_FULL toggle) the placed 本能的召唤
  (lure 4, exit 停顿 + DoT, 25 s redeploys while it runs); S3 y-8, per-second 神经损伤 until a burst, 1.5× burst recovery
  and the 迷狂牢笼 (blocks its tile's enemies at spawn / refresh, 1 per hit, 30 s after the skill).

Engine (additive, no handler before): ai.js hook `enemyAttackStart` (an enemy's normal attack, before 麻痹), blocking.js
flag `noNewBlock`, items/battle.js `holdsUndying` also sees a buff flagged `undying`; docs/SIM.md §3 / §5, kits/README.md
(docked skill-summon pieces: their kit without offOwner).

Golden: only diy moved — diy-106 … diy-109 (the prototype configurations shifted behind the new operators) and the new
diy-110 … diy-114 (the three operators' configurations); roster, bonds, fields, matches, standins unchanged.
Tests: test/content/op_slent2.test.js (11), op_haruka.test.js (9), op_phatm2.test.js (13).
This commit is contained in:
sganggs committed 2026-10-06 05:14:29 +08:00
1 parent 996f5d20fe
commit a61d82ee4c
13 files changed
+2408 -113

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);
});
+344 -108
View File
@@ -4495,128 +4495,128 @@
"snaps": "9e71a576 5f85bc01 c3ec0c6e 907b650d 8d0a1507 16eb2156 378d038b 6506577e 30807fb9 f2c86b9d ec752e72 d7303d83 83002a0a 01b70ca0 09e839c4 98d2358c 161de0dc 5f1fb139 3915cceb aeddf47d a8963bbe 76037450 84a29f92"
},
"diy-106": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 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": true,
"about": "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 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",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 352330692,
"waveId": "act1autochess_h05",
"time": 193.8,
"ticks": 5814,
"reason": "cleared",
"killed": 0,
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 2,
"total": 126,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":126,"perfect":false,"leaks":126,"leaksUncounted":0,"coins":0,"dmg":59922,"bossDmg":0,"heal":8548,"deaths":25,"layerGains":{}}},
"players": {"p1":{"killed":2,"total":126,"perfect":false,"leaks":124,"leaksUncounted":0,"coins":0,"dmg":185608,"bossDmg":0,"heal":222726,"deaths":6,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,3,0,1808,2128,0,8,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,6,0,9112,3634,0,34,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,83,0,1003,6580,0,8,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,86,0,14738,7835,0,17,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,153,0,9754,4473,0,27,1],
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,94,0,11704,3746,0,27,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,90,0,2802,2266,0,12,0],
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,79,0,3020,2720,0,11,0],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,141,0,0,5763,2109,6,0],
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,128,0,0,3807,6439,10,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,112,0,2354,1864,0,12,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,148,0,3628,4041,0,16,1],
["p1",1,"op","chess_char_5_diy1_a",0,null,1445,1,5,0,14876,0,20654,138,4],
["p1",2,"op","chess_char_5_diy2_a",1,null,1445,1,7,0,16819,7424,10116,121,4],
["p1",3,"op","chess_char_5_diy1_a",2,null,1445,1,9,1,8385,8332,22485,118,2],
["p1",4,"op","chess_char_5_diy2_b",0,"none",1765,1,146,0,9583,8995,12450,72,2],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1765,1,144,0,4277,6484,1974,20,0],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1765,1,6,0,9724,7959,24011,117,2],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_slent2",1865,1,8,0,15423,28653,31114,117,4],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_slent2",1865,1,10,0,20884,34427,15249,125,5],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_slent2",1865,1,142,0,7343,41668,14024,61,1],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_slent2",1915,1,145,0,11040,8846,2636,25,1],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_slent2",1915,1,3,1,42908,41401,23103,143,5],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_slent2",1915,1,4,0,24346,39528,44909,137,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_1251_lysyta_2":[54,0,54,8019,34461],"enemy_1252_lysytb_2":[42,0,42,35352,24551],"enemy_1329_cbshld_2":[30,0,30,5487,911]},
"hooks": [1,153,5814,380,380,358,358,16,25,151,5,5,0,8987,28,10,126,126,1,25,0,0,0],
"events": {"atk":380,"deploy":25,"die":25,"dmg":358,"heal":16,"leak":126,"skill":10,"spawn":140,"status":50},
"enemies": {"enemy_1251_lysyta_2":[54,2,52,72057,77511],"enemy_1252_lysytb_2":[42,0,41,161659,81275],"enemy_1329_cbshld_2":[30,0,30,0,26822]},
"hooks": [1,146,6900,1736,1736,1366,1366,2615,8,131,32,31,0,40172,0,3,126,123,1,8,0,0,0],
"events": {"atk":1952,"deploy":18,"die":8,"dmg":1366,"heal":586,"leak":123,"skill":63,"spawn":140},
"fx": {},
"snaps": "57186c39 a89f274b e649d7d9 75621917 006f6ba9 7f3b8dda bb3864de e8193348 16cc7f17 db8194aa e07167eb 0946e952 5d27c316 103f7135 f7e98be4 ef855c6a d34a4bb6 5d39faae c6e66696 77bde8df"
"snaps": "3676e362 98440adc d29d9678 d1a092e2 dde6eccc e8116940 26bf8723 4f17a27b b60deaa7 182b44c1 7def6f57 82d87d10 6edefabf 88403906 c9245b5f cc672aaf f3d8a9c3 15160316 cb82db41 3fc7005e 1ecf396e 16f5e8be 4317dc8e"
},
"diy-107": {
"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=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
"about": "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 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",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_multi_funny",
"round": 12,
"seed": 4020340196,
"waveId": "act1autochess_h05",
"time": 203.3,
"ticks": 6099,
"reason": "cleared",
"killed": 90,
"total": 90,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 450,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":90,"total":90,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":792864,"bossDmg":0,"heal":34212,"deaths":7,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,2604,1,106,0,2245,2604,0,9,1],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",2367.8,1,105,0,4281,4374,0,19,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,100,5,45221,3058,0,15,4],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,7,3,48414,1904,0,24,6],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,95,13,46166,11997,0,51,3],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",3411,1,8,3,33047,3326,0,19,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1272,1,2,0,13858,0,0,19,1],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,3,38,146945,0,0,177,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1372,1,4,1,19328,0,0,20,1],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1495.56,1,12,27,433358,24418,0,89,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1224,1,1,0,0,0,11831,36,2],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,5,0,0,0,22381,66,2]
],
"enemies": {"enemy_1229_darmy":[27,27,0,3192,108864],"enemy_1229_darmy_2":[51,51,0,12310,269280],"enemy_9007_acelem":[12,12,0,36180,414720]},
"hooks": [1,106,6099,594,594,1735,1777,160,97,97,28,27,0,32480,0,62,90,0,1,97,0,2,0],
"events": {"atk":764,"deploy":16,"die":97,"dmg":1777,"fx":154,"heal":142,"skill":55,"spawn":102,"status":62},
"fx": {"aoe":52,"beam":25,"burst":2,"dp":18,"slash":57},
"snaps": "03984647 ace8dbe4 49520875 0ad8fbe6 ba7c56bb e58d0120 a539ef32 0e2bc9be 3b333b61 db04449a dace616b 63a39256 5c34a56e d1619dfd 63bd653a d74deb99 1d733ab1 6e8cbc08 04eb4bb7 74b3c8bb 80dc47fc"
},
"diy-108": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_single_hard",
"round": 13,
"seed": 1366160473,
"seed": 4020340196,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 15,
"total": 36,
"killed": 18,
"total": 96,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 95,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":15,"total":36,"perfect":false,"leaks":21,"leaksUncounted":0,"coins":0,"dmg":223366,"bossDmg":0,"heal":0,"deaths":32,"layerGains":{}}},
"players": {"p1":{"killed":18,"total":96,"perfect":false,"leaks":78,"leaksUncounted":0,"coins":0,"dmg":309522,"bossDmg":0,"heal":161909,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,3,0,0,2604,0,0,0],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,4,0,0,3431,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,5,1,41288,1865,0,28,1],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,8,2,117437,2374,0,51,1],
["p1",5,"op","chess_char_6_diy1_a",2,null,1051.96,1,71,0,9905,3535,0,0,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,68,0,22211,5712,0,0,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,72,0,0,10386,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,73,0,0,10136,0,0,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,43,0,0,5276,0,0,0],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,52,0,0,7062,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,6,0,13315,1160,0,28,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,54,0,19210,2860,0,23,1],
["p1",1,"op","chess_char_6_diy1_a",0,null,1734,1,2,1,15287,0,13792,123,4],
["p1",2,"op","chess_char_6_diy2_a",1,null,1734,1,3,1,20801,0,8338,132,5],
["p1",3,"op","chess_char_6_diy1_a",2,null,1734,1,4,1,11753,0,15557,122,2],
["p1",4,"op","chess_char_6_diy2_b",0,"none",2020.58,1,6,4,24275,19524,5954,129,3],
["p1",5,"op","chess_char_6_diy1_b",1,"none",2020.58,1,9,1,25121,20347,10761,142,5],
["p1",6,"op","chess_char_6_diy2_b",2,"none",1905.35,1,10,1,17406,18845,18603,138,2],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_slent2",2298,1,7,3,21116,17210,3629,126,2],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_slent2",2298,1,11,0,31212,20397,11177,146,5],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_slent2",2104.88,1,8,4,18991,18826,16507,129,2],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_slent2",2164.88,1,12,1,45880,20182,20379,132,4],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_slent2",983.64,1,1,0,48794,13352,10137,123,5],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_slent2",2260.58,1,5,1,28886,15099,27076,116,2]
],
"enemies": {"enemy_1229_darmy":[27,6,21,14196,47091],"enemy_1229_darmy_2":[57,12,45,41889,122813],"enemy_9007_acelem":[12,0,4,107697,139618]},
"hooks": [1,108,6900,1816,1816,2815,2998,863,18,88,41,37,0,45286,0,25,96,70,1,18,0,16,0],
"events": {"atk":2153,"deploy":12,"die":18,"dmg":2883,"fx":30,"heal":749,"leak":70,"skill":78,"spawn":108,"status":52},
"fx": {"beam":8,"burst":16,"shield":6},
"snaps": "86275aff 3cbcc233 78269a94 b294ecf6 fade6650 6c17f1a0 4cd7b158 6295dc91 d2055a58 5d067d3b 69caf026 a4f1ea19 d87b1caa d710a6c0 5c02d442 fca0179a 8945b551 1a74a975 2a6f940e d9092028 39b7178c 0a605cbc 4c73b390"
},
"diy-108": {
"about": "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 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",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_single_hard",
"round": 12,
"seed": 1366160473,
"waveId": "act1autochess_h05",
"time": 219.47,
"ticks": 6584,
"reason": "cleared",
"killed": 11,
"total": 72,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":11,"total":72,"perfect":false,"leaks":61,"leaksUncounted":0,"coins":0,"dmg":107032,"bossDmg":0,"heal":58800,"deaths":13,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,1415,1,95,0,4040,2865,2585,29,3],
["p1",2,"op","chess_char_5_diy2_a",1,null,1415,1,94,1,15960,3678,7269,34,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,85,1,5000,4053,3620,31,1],
["p1",4,"op","chess_char_5_diy2_b",0,"none",1728,1,97,0,5379,1971,1858,31,2],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1728,1,96,3,24743,5809,4362,25,2],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1728,1,93,0,4430,12096,8282,42,2],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_haruka",1858,1,83,3,7725,30129,12457,47,3],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_haruka",1858,1,91,2,16891,3106,2045,18,3],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_haruka",1858,1,86,0,11592,3664,8799,42,2],
["p1",10,"op","chess_char_6_diy1_a",0,null,1415,1,90,0,1811,4014,2323,19,3],
["p1",11,"op","chess_char_6_diy2_a",1,null,1415,1,88,1,5044,6549,2321,16,3],
["p1",12,"op","chess_char_6_diy1_a",2,null,1415,1,98,0,4418,1528,2879,30,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]
],
"enemies": {"enemy_1005_yokai_2":[12,12,0,102,639],"enemy_1005_yokai_3":[12,0,11,29968,87430],"enemy_9009_acfort":[12,3,9,26331,135297]},
"hooks": [1,73,6900,439,439,497,550,0,47,67,8,24,0,10810,814,0,36,20,1,35,0,0,17],
"events": {"atk":472,"deploy":35,"die":47,"dmg":550,"fx":92,"leak":20,"skill":48,"spawn":50,"status":162},
"fx": {"beam":35,"dodge":21,"dp":4,"slash":32},
"snaps": "7c7487b3 43a949cf 206930da 90551654 2113b5a2 a7c4855b 59932997 42395489 3f83a15d ffbd721d 646072f3 fc3be7c4 b210ec54 960a9e51 aa1af455 09ccfce3 50e6e3d0 380e639b 9eaf5bef 3b943f89 3933d1fc 3718a2f5 a8ae4307"
"enemies": {"enemy_1005_yokai_2":[18,11,7,7065,27361],"enemy_1041_lazerd_2":[24,0,24,72398,31409],"enemy_1042_frostd":[30,0,30,0,48262]},
"hooks": [1,98,6584,705,705,736,736,338,24,85,27,26,0,25949,0,0,72,61,1,24,0,0,0],
"events": {"atk":767,"deploy":24,"die":24,"dmg":736,"fx":403,"heal":291,"leak":61,"skill":53,"spawn":86,"status":405},
"fx": {"aoe":68,"heal":159,"shield":176},
"snaps": "99ca2177 7bb9d77d 0dfcc8a6 a1132d55 63f65478 674aac09 b2edf22b 1fd88e3c 5a61ac4e 17b0c73f 53e9b3ba 6922d432 9633466d 587d1707 8392c836 7cb1cb23 68e62086 1d63b014 ff295bb6 bf3cb8f8 88253959 8c58ac6b"
},
"diy-109": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
"about": "6_diy1_b=haruka/S1 6_diy2_b=haruka/S2 6_diy1_b=haruka/S3 6_diy2_b=haruka/S1/002_haruka 6_diy1_b=haruka/S2/002_haruka 6_diy2_b=haruka/S3/002_haruka 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",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m03",
@@ -4624,38 +4624,274 @@
"round": 13,
"seed": 500258608,
"waveId": "act1autochess_h06",
"time": 215.33,
"ticks": 6460,
"time": 219.53,
"ticks": 6586,
"reason": "cleared",
"killed": 15,
"killed": 36,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 453,
"rngDraws": 66,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":15,"total":123,"perfect":false,"leaks":108,"leaksUncounted":0,"coins":0,"dmg":280261,"bossDmg":0,"heal":30749,"deaths":28,"layerGains":{}}},
"players": {"p1":{"killed":36,"total":123,"perfect":false,"leaks":87,"leaksUncounted":0,"coins":0,"dmg":620589,"bossDmg":0,"heal":42614,"deaths":19,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,5,0,1376,2285,0,5,0],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,7,0,982,2494,0,2,0],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,155,2,36128,9372,0,79,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,156,2,52788,20167,0,62,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,140,4,59650,2964,0,77,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,154,6,91210,11325,0,66,1],
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,86,0,3596,3133,0,4,0],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,83,0,19042,8583,0,5,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,4,0,0,2326,11819,20,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,10,0,0,4670,18930,25,1],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,108,0,5752,2320,0,16,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,8,1,9738,1450,0,16,1],
["p1",1,"op","chess_char_6_diy1_b",0,"none",1728,1,142,4,12457,6113,6767,67,3],
["p1",2,"op","chess_char_6_diy2_b",1,"none",0,0,136,1,28364,7807,7345,38,2],
["p1",3,"op","chess_char_6_diy1_b",2,"none",0,0,133,0,13951,5593,5069,41,1],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_haruka",0,0,129,0,13306,21202,8069,40,2],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_haruka",0,0,147,1,29509,6121,5732,29,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_haruka",1938,1,139,1,66445,7552,9632,53,2],
["p1",7,"op","chess_char_5_diy1_a",0,null,936,1,149,0,2663,936,0,7,1],
["p1",8,"op","chess_char_5_diy2_a",1,null,936,1,140,1,17177,1175,0,20,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,936,1,165,12,153864,1740,0,65,2],
["p1",10,"op","chess_char_5_diy2_b",0,"none",1111,1,167,1,22388,2679,0,20,5],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,157,0,9241,3915,0,11,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1111,1,144,15,251225,5584,0,68,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],
["p1","~1","token","token_10055_phatm2_mndclv",null,null,0,0,35,0,0,3,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,0,0,41,0,0,3,0,0,null],
["p1","~4","token","token_10055_phatm2_mndclv",null,null,3,0,42,0,0,0,0,0,null],
["p1","~5","token","token_10055_phatm2_mndclv",null,null,2,0,43,0,0,1,0,0,null],
["p1","~6","token","token_10055_phatm2_mndclv",null,null,0,0,46,0,0,7,0,0,null],
["p1","~7","token","token_10055_phatm2_mndclv",null,null,0,0,47,0,0,3,0,0,null],
["p1","~8","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~9","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~10","token","token_10055_phatm2_mndclv",null,null,0,0,48,0,0,3,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,0,0,0,0,0,0,null],
["p1","~13","token","token_10055_phatm2_mndclv",null,null,0,0,55,0,0,4,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,1,0,96,0,0,7,0,0,null],
["p1","~16","token","token_10055_phatm2_mndclv",null,null,0,0,100,0,0,3,0,0,null],
["p1","~17","token","token_10055_phatm2_mndclv",null,null,2,0,159,0,0,1,0,0,null],
["p1","~18","token","token_10055_phatm2_mndclv",null,null,1,0,161,0,0,7,0,0,null],
["p1","~19","token","token_10055_phatm2_mndclv",null,null,3,0,162,0,0,0,0,0,null],
["p1","~20","token","token_10055_phatm2_mndclv",null,null,3,0,164,0,0,0,0,0,null],
["p1","~21","token","token_10055_phatm2_mndclv",null,null,3,0,166,0,0,0,0,0,null]
],
"enemies": {"enemy_1019_jshoot_2":[57,23,34,66059,205652],"enemy_1299_ymkilr_2":[27,1,26,3752,119405],"enemy_1422_lrsldr_2":[39,12,27,648,295533]},
"hooks": [1,167,6586,794,794,1044,1878,238,62,157,25,24,0,18889,116,69,123,87,1,62,0,71,0],
"events": {"atk":856,"deploy":41,"die":70,"dmg":1878,"fx":522,"heal":199,"leak":87,"skill":49,"spawn":153,"status":559},
"fx": {"aoe":41,"burst":71,"heal":119,"levitate":39,"palsy":99,"shield":138,"summon":15},
"snaps": "38675e3b ce483b00 bb7365a2 ca64412f a0f0e83e c62de0fc a949ee05 6b5afc5d d23996c5 98580a83 42ddc3cb 3f0ae9f3 3653eb16 85dc6e09 15f617ef 452fe8f3 841aad0b 26c7dfee 6cf48c28 45c2b46f 55b142cb 4a1af6e6"
},
"diy-110": {
"about": "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 6_diy2_b=phatm2/S1 6_diy1_b=phatm2/S2 6_diy2_b=phatm2/S3 6_diy1_b=phatm2/S1/002_phatm2 6_diy2_b=phatm2/S2/002_phatm2 6_diy1_b=phatm2/S3/002_phatm2",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_multi_hard",
"round": 13,
"seed": 2140781790,
"waveId": "act1autochess_h06",
"time": 223.03,
"ticks": 6691,
"reason": "cleared",
"killed": 2,
"total": 144,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":2,"total":144,"perfect":false,"leaks":142,"leaksUncounted":0,"coins":0,"dmg":136153,"bossDmg":0,"heal":0,"deaths":26,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_phatm2",0,0,174,0,35368,3708,0,16,3],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_phatm2",0,0,175,0,9451,3708,0,13,1],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_phatm2",0,0,158,0,2731,3708,0,11,0],
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,169,0,1459,2808,0,6,1],
["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,150,1,3788,1872,0,13,1],
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,104,0,2836,1872,0,12,0],
["p1",7,"op","chess_char_6_diy2_b",0,"none",0,0,91,0,8250,2222,0,8,1],
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,88,0,1556,2222,0,7,0],
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,98,0,20898,2222,0,10,1],
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_phatm2",0,0,6,1,14988,1311,0,9,2],
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_phatm2",0,0,7,0,14020,1311,0,8,0],
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_phatm2",0,0,97,0,20808,2622,0,11,0],
["-","~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],
["p1","~1","token","token_10055_phatm2_mndclv",null,null,3,0,35,0,0,0,0,0,null],
["p1","~2","token","token_10055_phatm2_mndclv",null,null,3,0,36,0,0,0,0,0,null],
["p1","~3","token","token_10055_phatm2_mndclv",null,null,3,0,37,0,0,0,0,0,null]
],
"enemies": {"enemy_1251_lysyta_2":[51,2,49,0,67314],"enemy_1252_lysytb_2":[54,0,54,29586,58759],"enemy_1329_cbshld_2":[39,0,39,0,44360]},
"hooks": [1,175,6691,199,199,1057,1375,0,28,173,10,10,0,5621,25,9,144,142,1,28,0,19,0],
"events": {"atk":199,"deploy":29,"die":31,"dmg":1375,"fx":38,"leak":142,"skill":20,"spawn":161,"status":60},
"fx": {"burst":19,"palsy":16,"summon":3},
"snaps": "14bbfa37 cfa63019 4fa48ed3 57e3666c baf7eebe 113d1868 50f73e64 4e5825ed 34a0212e abbfd75d 06def83e fd22dda1 ee992808 0c16dae5 155e77eb a88aa5ad 00b11fa4 13497a89 43783b6c dbb3f0a6 08ecdadc 9506da45 33802f69"
},
"diy-111": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 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": true,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_single_normal",
"round": 12,
"seed": 452407399,
"waveId": "act1autochess_h05",
"time": 216.83,
"ticks": 6505,
"reason": "cleared",
"killed": 13,
"total": 99,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":13,"total":99,"perfect":false,"leaks":86,"leaksUncounted":0,"coins":0,"dmg":304937,"bossDmg":0,"heal":8682,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,123,0,1472,6384,0,32,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,81,1,11090,5816,0,90,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,120,1,19609,7692,0,33,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",3334,1,124,4,33655,6668,0,37,1],
["p1",5,"op","chess_char_5_diy1_a",2,null,1271,1,4,4,51924,0,0,185,3],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,5,1,71779,0,0,187,3],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,11,1,41478,0,0,153,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,13,0,49245,0,0,159,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,6,0,0,0,8682,26,3],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,14,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,114,0,3950,2796,0,17,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,86,1,20735,10944,0,78,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,10,47,52058,82202],"enemy_1299_ymkilr_2":[27,0,27,9819,9240],"enemy_1422_lrsldr_2":[39,5,34,9213,188819]},
"hooks": [1,156,6460,632,632,849,850,92,43,151,7,20,0,9133,611,30,123,108,1,43,0,0,0],
"events": {"atk":705,"deploy":30,"die":43,"dmg":822,"fx":36,"heal":76,"leak":108,"skill":42,"spawn":138,"status":149},
"fx": {"aoe":2,"displace":17,"dodge":2,"spGain":15},
"snaps": "17351197 d85b764f 170d4969 276e2353 d5273c76 84c8775e 178ead82 e788140a 7761c24b 8d4c0668 9647f92d 7caa4901 24e1ffd3 00675a6a 82e3afd4 846d95f4 87dfd927 4f075077 94a7b0a4 a3a639d3 4179fb65 3ad0ed6c"
"enemies": {"enemy_1010_demon":[42,2,40,21226,115683],"enemy_1320_wdrrl_2":[12,0,12,5886,12532],"enemy_1422_lrsldr_2":[45,11,34,13189,176722]},
"hooks": [1,124,6505,1247,1247,1221,1221,26,28,114,20,19,0,28400,94,34,99,86,1,28,0,0,0],
"events": {"atk":1247,"deploy":22,"die":28,"dmg":1221,"fx":1,"heal":26,"leak":86,"skill":39,"spawn":114,"status":187},
"fx": {"explode":1},
"snaps": "c483ab5c c43d24d1 3012b5e1 e015a05e 6b17b1fe 40152fac 120f3052 92460f9e 6d430249 0820bac0 f6e91bf2 f81209a2 fa43731e 7086293d cd16e934 56c0a32c e19ec163 5a5ef609 334df640 1a69c7ed 52f1c2f5 da5425f6"
},
"diy-112": {
"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=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 3106580722,
"waveId": "act1autochess_h05",
"time": 197.6,
"ticks": 5928,
"reason": "cleared",
"killed": 9,
"total": 135,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 204,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":9,"total":135,"perfect":false,"leaks":126,"leaksUncounted":0,"coins":0,"dmg":415122,"bossDmg":0,"heal":16340,"deaths":22,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,3,1,3246,2604,0,25,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,1,1,14125,3371,0,45,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,124,2,41582,6571,0,29,3],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,104,3,101035,6650,0,27,3],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,103,0,21884,5680,0,19,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,101,2,65446,11148,0,33,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,111,0,20912,3294,0,22,1],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",119.44,1,158,0,25637,5375,0,39,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,6,0,68224,2164,0,14,1],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,95,0,53031,4607,0,13,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,141,0,0,4932,1260,3,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,153,0,0,3438,15080,14,1]
],
"enemies": {"enemy_2021_syfish":[105,9,96,13256,339526],"enemy_2025_syufo":[30,0,30,46578,75596]},
"hooks": [1,158,5928,586,586,945,1056,33,31,157,15,15,0,13917,0,30,135,126,1,59,0,16,29],
"events": {"atk":632,"deploy":23,"die":31,"dmg":1056,"fx":153,"heal":32,"leak":126,"skill":30,"spawn":147,"status":66},
"fx": {"aoe":42,"burst":16,"dodge":29,"dp":11,"slash":48,"spGain":7},
"snaps": "f1fffa0d 2e4ead28 c68dbfb0 75d60e92 28dd7c37 394efa53 2f2aabd0 2343aed0 5ead163f 3efc95b3 7649823f e5961d99 7f86e2ed 8c75f677 2b7c65c1 de7b4948 22db0221 4ff706e7 f02a4505 f70972db"
},
"diy-113": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_multi_funny",
"round": 13,
"seed": 1466195980,
"waveId": "act1autochess_h06",
"time": 200.47,
"ticks": 6014,
"reason": "cleared",
"killed": 69,
"total": 144,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 203,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":69,"total":144,"perfect":false,"leaks":75,"leaksUncounted":0,"coins":0,"dmg":508542,"bossDmg":0,"heal":0,"deaths":24,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,4,4,17615,2604,0,49,1],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,5,6,24416,3431,0,48,1],
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,160,6,53183,3730,0,70,1],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,155,15,157188,4748,0,87,1],
["p1",5,"op","chess_char_6_diy1_a",2,null,41,1,172,5,40925,4546,0,21,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1805,1,175,16,100705,3907,0,33,4],
["p1",7,"op","chess_char_5_diy1_a",2,null,1121,1,176,1,25672,9265,0,50,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1185,1,121,4,15855,1711,0,34,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,2041,1,166,4,18403,3235,0,22,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",1250,1,143,6,17122,5812,0,14,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,178,1,17171,1160,0,53,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,135,1,20287,2860,0,42,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_2021_syfish":[99,58,41,7589,243386],"enemy_2025_syufo":[45,11,34,39420,265157]},
"hooks": [1,178,6014,1142,1142,1613,2045,0,93,168,14,23,0,18585,1230,71,144,75,1,93,0,32,16],
"events": {"atk":1184,"deploy":31,"die":93,"dmg":2031,"fx":189,"leak":75,"skill":49,"spawn":159,"status":356},
"fx": {"aoe":60,"burst":32,"displace":5,"dodge":22,"dp":27,"slash":43},
"snaps": "de90f0a5 759b0c3a c39a5b8a fa9acb47 839ff747 a9da663c 92684f45 89723c39 48fd5534 c8914ffd f805490d c236abdb f8e576f2 c08df0a5 c194716c e8ee08eb 096b0ab7 b3b92f7b b13fcc00 9be81275"
},
"diy-114": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m04",
"modeId": "mode_single_hard",
"round": 13,
"seed": 4120508441,
"waveId": "act1autochess_h06",
"time": 205.67,
"ticks": 6170,
"reason": "cleared",
"killed": 122,
"total": 192,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 991,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":122,"total":192,"perfect":false,"leaks":70,"leaksUncounted":0,"coins":0,"dmg":670162,"bossDmg":0,"heal":46494,"deaths":25,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,178,5,43295,3934,0,51,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,6,22,13148,2494,0,39,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,219,2,21846,8078,0,19,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,224,5,52253,13200,0,51,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,4,23,79207,0,0,180,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,5,35,114330,0,0,186,2],
["p1",7,"op","chess_char_6_diy1_a",2,null,1372,1,15,4,216108,0,0,75,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,205,9,72144,3638,0,33,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,190,0,0,2908,11119,25,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,12,0,0,24392,35374,57,2],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,139,1,13963,4532,0,46,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,14,2,43869,19327,0,91,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_1197_sfshu": [15,11,4,1177,60295],
"enemy_1197_sfshu_2": [39,22,17,11441,182028],
"enemy_1198_msfshu": [33,28,5,0,87],
"enemy_1198_msfshu_2": [66,51,15,0,216],
"enemy_9010_acpupp": [39,10,29,60856,505701]
},
"hooks": [1,224,6170,969,969,3329,3121,155,147,217,15,29,0,21470,5920,45,192,70,1,161,0,0,0],
"events": {"atk":1309,"deploy":29,"die":147,"dmg":3064,"fx":156,"heal":114,"leak":70,"skill":56,"spawn":207,"status":688},
"fx": {"aoe":18,"displace":24,"dodge":5,"ember":14,"revive":2,"shieldBreak":41,"spGain":5,"summon":33,"telegraph":14},
"snaps": "052f6301 53938868 38f85ad5 39e3b0ee 87b525a1 113be68f a004c4d3 f24bde7d 54cd2c7c 0ed4e64a f8ceb24d 42d7ad5a e71b7cd4 6db6b3d9 6594d286 6a111077 2d0b44a1 366e5b47 86d5661e 095a92d8 3492d771"
}
}
}