feedback5 O21: 自选 kits for 麦哲伦, 令 and 电弧 (with their summons) and the 召唤师 summon deck

- kits/ops/op-mgllan.js, op-ling.js, op-radian.js: every skill, both talents, the trait and every module at every form,
  and the kits of their summons (龙腾.F / .L / .A, “清平” / “逍遥” / “弦惊”, 戴乌 / 赛柯 / 桑特拉).
- kits/shared/summoner.js (new): the summon deck the three share — the holding (charge_token[born], SUM-Y's +3), the
  return of a placed piece that left the field (owner on the field, redeploy time, one held, DP), recalls, the summons
  leaving with their owner (die_to_kill_token).
- kits/index.js: OPERATOR_KIT_FILES += op-mgllan.js, op-ling.js, op-radian.js; kits/README.md: the layout row and a
  pointer for summoner kits.
- tests: test/content/op_mgllan.test.js, op_ling.test.js, op_radian.test.js.
- golden: only diy moved — diy-106 … diy-109 now field the new kits' configurations (operator configurations are appended
  in OPERATOR_KIT_FILES order, so the prototype chunks shift back) and diy-110 … diy-115 are new; every other family is
  byte-identical.
This commit is contained in:
sganggs committed 2026-10-06 05:11:04 +08:00
1 parent 996f5d20fe
commit f0d220166a
10 files changed
+2150 -97

No files matched your search

+297
View File
@@ -0,0 +1,297 @@
// test/content/op_ling.test.js — the 自选 operator kit of 令 (char_2023_ling, 6★ 召唤师; kit
// server/sim/content/kits/ops/op-ling.js, the summon deck of kits/shared/summoner.js) and of her summons “清平” / “逍遥” /
// “弦惊”, fielded the production way (a DIY slot + its `diy` pick, simdata getDiy; the summons as the placed hand pieces 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
// SUM-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_ling.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 LING = 'char_2023_ling';
const FORMS = BACKUPS.units[LING].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const SUMY = 'uniequip_002_ling';
const S1 = 'skchr_ling_1', S2 = 'skchr_ling_2', S3 = 'skchr_ling_3';
const Q = 'token_10020_ling_soul1', X = 'token_10020_ling_soul2', J = 'token_10020_ling_soul3';
const SOUL_OF = [Q, X, J];
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 tokOf = (tok, tier, elite, skill, mod) => {
let v = BACKUPS.tokens[tok].variants[`${LING}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
return v;
};
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, SUMY].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 令 as uid 1 at (row, col) facing RIGHT; `souls` = [[tokenId, row, col, dir?]] placed pieces (uid 10 +). */
function field({ tier = 5, elite = false, mod = null, skill = 0, souls = [], row = 10, col = 3, seed = 5, dp = 99 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999, dpInit: dp }, captureNoisy: true,
hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack', 'dodge'],
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: LING, skillIndex: skill, uniEquipId: mod }, elite, row, col },
...souls.map(([tokenId, r, c, dir], i) => ({ uid: 10 + i, kind: 'token', tokenId, ownerUid: 1, row: r, col: c, dir: dir ?? 'RIGHT' }))],
});
h.step();
const u = h.unit(1);
return { h, u, ss: h.b.allyUnits.filter((a) => a.kind === 'token'), deck: u.mem.summonDeck };
}
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('令 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, ranged arts, hits air, blocks 1, ground-targetable, the data triggers (DEFAULT), 炎, no 特质', () => {
assert.equal(OPERATOR_KITS[LING], KITS[LING]);
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], [LING, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 召唤师`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
assert.deepEqual([u.skill.rule, form.skills[skill].trigger.rule], ['DEFAULT', 'DEFAULT'], `${label(f)}: the data's trigger`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['yanShip'], []], `${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);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 852 / 406 / 114, E2 Lv60 1002 / 450 / 130; SUM-Y +100 / +30 → +150 / +50 HP / ATK
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], [852, 406, 1002, 450]);
assert.deepEqual([modOf(5, SUMY).attr, modOf(6, SUMY).attr], [{ maxHp: 100, atk: 30 }, { maxHp: 150, atk: 50 }]);
});
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(LING));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(LING), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: LING, skillIndex: 1, uniEquipId: SUMY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: LING, skillIndex: 1, uniEquipId: SUMY } } });
});
test('T1 挑灯问梦: her skill\'s summon is her piece; the deck — 5 held, cap 5 (8 with SUM-Y: +3 on the card), at most 3 standing (4 at SUM-Y stage 3); every summon holds 禁疗 and fights with its data (清平 1-1 melee physical blocks 1, 逍遥 3-1 arts hits air, 弦惊 1-1 blocks 2 strikes all it blocks)', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const tok = SOUL_OF[skill];
const { h, u, ss, deck } = field({ tier, elite, mod, skill, souls: [[tok, 10, 6]] });
const s = ss[0], v = tokOf(tok, tier, elite, skill, mod);
const y = elite && mod === SUMY;
assert.ok(h.b.producesToken(u, tok) && SOUL_OF.filter((x) => x !== tok).every((x) => !h.b.producesToken(u, x)), `${label(f)} S${skill + 1}: only ${tok}`);
assert.deepEqual([deck.charge, deck.cap, deck.held, deck.maxDeployed], [5, y ? 8 : 5, y ? 8 : 5, y && tier === 6 ? 4 : 3], `${label(f)} S${skill + 1}: deck`);
assert.deepEqual([s.base.maxHp, s.base.atk, s.base.def, s.base.res, s.base.cost, s.base.respawnTime, s.base.bat], [v.stats.maxHp, v.stats.atk, v.stats.def, v.stats.res, v.stats.cost, v.stats.respawnTime, v.stats.bat], `${label(f)}: summon stats`);
assert.deepEqual([s.alive, !!s.s.flags.noHeal, s.kit.skill], [true, true, null], `${label(f)}: deployed, 禁疗, no skill`);
assert.deepEqual(s.liveRangeGrid, v.rangeGrid, `${label(f)}: its range`);
const want = { [Q]: ['melee', 'phys', false, 1, false], [X]: ['ranged', 'arts', true, 1, false], [J]: ['melee', 'phys', false, 2, true] }[tok];
assert.deepEqual([s.profile.attack, s.profile.dmgType, s.profile.canHitFly, s.s.blockCnt, !!s.profile.hitAllBlocked], want, `${label(f)}: ${tok}`);
done(h);
}
}
// SUM-Y: 清平 / 逍遥 −3 DP, 弦惊 −5 ("部署费用-3 / -5"); stage 3 清平 +150 HP +45 ATK, 逍遥 +100 / +35, 弦惊 +250 / +60
assert.deepEqual(SOUL_OF.map((t, i) => tokOf(t, 6, false, i, null).stats.cost - tokOf(t, 6, true, i, SUMY).stats.cost), [3, 3, 5]);
assert.deepEqual(SOUL_OF.map((t, i) => [tokOf(t, 6, true, i, SUMY).stats.maxHp - tokOf(t, 6, true, i, null).stats.maxHp, tokOf(t, 6, true, i, SUMY).stats.atk - tokOf(t, 6, true, i, null).stats.atk]), [[150, 45], [100, 35], [250, 60]]);
});
test('T2 随付笺咏醉屠苏: a summon knocked out, recalled or absorbed ⇒ +3 SP and a stack of ATK +3 % (at most 5); not when her summons leave with her; no SP while a timed skill of hers runs', () => {
const { h, u, ss } = field({ tier: 6, elite: true, skill: 0, souls: [[Q, 10, 6], [Q, 11, 6], [Q, 9, 6]] });
const t1 = formOf(6, true).talents.find((t) => t.index === 1).bb;
assert.deepEqual(t1, { max_stack_cnt: 5, sp: 3, atk: 0.03 });
const atk0 = u.s.atk;
const sp0 = u.skill.sp;
h.b.kill(ss[0], null);
approx(u.skill.sp - sp0, 3, '+3 SP', 1e-3);
assert.equal(u.findBuff('ling:t2').stacks, 1, 'one stack');
approx(u.s.atk, atk0 * 1.03, 'ATK +3 %');
for (let i = 0; i < 6; i++) { h.b.kill(ss[1 + (i % 2)], null); h.runUntil(() => ss.every((s) => s.alive), 15); }
assert.equal(u.findBuff('ling:t2').stacks, 5, 'at most 5 stacks');
approx(u.s.atk, atk0 * 1.15, 'ATK +15 %');
// no SP while S1 runs
h.spawn('enemy_dummy', { pos: [10, 4] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'S1 cast');
const spOn = u.skill.sp;
h.b.kill(ss[0], null);
assert.equal(u.skill.sp, spOn, 'no SP during a timed skill');
// her leaving: the summons go with her, nothing more
const st = u.findBuff('ling:t2').stacks;
h.b.retreat(u);
assert.equal(u.findBuff('ling:t2'), null, 'the stacks leave with her');
assert.equal(st, 5);
done(h);
});
test('S1 重进酒: +1 held at the cast; she and her summons ATK +29 % / +38 % and ASPD +29 / +38 for 25 s; her summons\' attacks deal arts damage meanwhile, physical before and after', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
const sk = skillOf(tier, elite, S1);
const { h, u, ss, deck } = field({ tier, elite, mod, skill: 0, souls: [[Q, 10, 6]] });
const s = ss[0];
assert.deepEqual([sk.bb.atk, sk.bb.attack_speed, sk.bb.cnt, sk.duration], [elite ? 0.38 : 0.29, elite ? 38 : 29, 1, 25]);
h.spawn('enemy_dummy', { pos: [10, 7] }); // its 1-1 (outside her 3-1)
h.spawn('enemy_dummy', { pos: [11, 4] }); // hers
h.run(3);
assert.ok(from(h, s).length && from(h, s).every((c) => c.type === 'phys'), 'physical before');
deck.held = 2;
const atk0 = u.s.atk, satk0 = s.s.atk;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
assert.equal(deck.held, 3, '技能开启时获得1个召唤物');
approx(u.s.atk, atk0 * (1 + sk.bb.atk), 'her ATK');
approx(s.s.atk, satk0 * (1 + sk.bb.atk), 'its ATK');
assert.deepEqual([u.s.aspd, s.s.aspd], [100 + sk.bb.attack_speed, 100 + sk.bb.attack_speed], 'ASPD');
const n0 = from(h, s).length;
h.run(4);
assert.ok(from(h, s).slice(n0).length && from(h, s).slice(n0).every((c) => c.type === 'arts'), 'arts during S1');
// knocked out during it: back 10 s later with the skill's bonus and arts
h.b.kill(s, null);
assert.ok(h.runUntil(() => s.alive, 10.5) && u.skill.active, 'back while it runs');
assert.deepEqual(s.findBuff('ling:s1:soul')?.mods, { atkPct: sk.bb.atk, aspd: sk.bb.attack_speed }, 'a summon deployed during it takes the bonus');
h.runUntil(() => !u.skill.active, 30);
approx(h.b.time, h.hooksOf('skillStart').at(-1).skill.lastStart + 25, '25 s', 0.01);
const n1 = from(h, s).length;
h.run(3);
assert.ok(from(h, s).slice(n1).every((c) => c.type === 'phys') && s.s.aspd === 100, 'physical and its ASPD back');
done(h);
}
});
test('S2 笑鸣瑟 (2 charges): her cast-attack strikes 2 enemies for 310 % / 370 % ATK arts and 束缚 2 / 2.5 s each; every summon of hers breaks off and strikes 2 enemies of its range for its 2.atk_scale × its ATK + 束缚 2.duration s (silenced / disarmed ones too); 1.2 s later the summons below half HP are recalled (+1 held, T2)', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
const sk = skillOf(tier, elite, S2);
const { h, u, ss, deck } = field({ tier, elite, mod, skill: 1, souls: [[X, 10, 5], [X, 11, 5], [X, 9, 5]] });
const [hurt, muted, fine] = ss;
const tv = tokOf(X, tier, elite, 1, mod);
assert.deepEqual([u.skill.maxCharges, sk.bb.atk_scale, sk.bb['ling_s2_unmovable.duration'], sk.bb.value, sk.bb.hp_ratio, tv.skill.bb['2.atk_scale'], tv.skill.bb['2.duration']],
[2, elite ? 3.7 : 3.1, elite ? 2.5 : 2, 2, 0.5, elite ? 3.7 : 3.1, elite ? 2.5 : 2]);
const a = h.spawn('enemy_dummy', { pos: [10, 5] }), b = h.spawn('enemy_dummy', { pos: [10, 6] }), c = h.spawn('enemy_dummy', { pos: [11, 6] });
hurt.hp = hurt.s.maxHp * 0.45;
fine.hp = fine.s.maxHp * 0.55;
h.b.applyStatus(muted, 'silence', { duration: 30, force: true });
h.b.applyStatus(muted, 'disarm', { duration: 30, force: true });
deck.held = 1;
u.skill.gainSp(999);
const sp0 = u.skill.sp;
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), 'cast on her attack');
const castAt = h.b.time;
h.run(0.6);
const mine = from(h, u).filter((x) => x.dmg.isSkill);
assert.equal(mine.length, 2, 'her two targets');
for (const x of mine) approx(x.amount, u.s.atk * sk.bb.atk_scale, 'her scale');
const binds = h.hooksOf('statusApplied').filter((x) => x.status === 'bind');
assert.ok(binds.some((x) => x.source === u) && binds.filter((x) => x.source === u).every((x) => Math.abs(x.duration - sk.bb['ling_s2_unmovable.duration']) < 1e-9), 'her 束缚');
for (const s of ss) {
const hits = from(h, s).filter((x) => x.dmg.tags?.includes('ling:s2'));
assert.equal(hits.length, 2, `${s.uid}: two targets`);
for (const x of hits) approx(x.amount, s.s.atk * tv.skill.bb['2.atk_scale'] * (1 - x.target.s.res / 100), `${s.uid}: its scale`);
assert.ok(binds.filter((x) => x.source === s).length === 2, `${s.uid}: 束缚 ×2`);
}
const struck = new Set(h.hooksOf('damaged').filter((x) => x.dmg?.isSkill).map((x) => x.target));
assert.ok(struck.size >= 2 && [...struck].every((e) => e.s.flags.bind), 'every struck enemy bound');
void a; void b; void c;
// 1.2 s after the cast: the summon below half HP is recalled (T2: +3 SP, a stack)
h.runUntil(() => h.b.time >= castAt + 1.1, 2);
assert.ok(hurt.alive, 'not before 1.2 s');
const spBefore = u.skill.sp;
assert.ok(h.runUntil(() => !hurt.alive, 0.2), 'recalled at 1.2 s');
assert.deepEqual([hurt.alive, muted.alive, fine.alive], [false, true, true], 'only the one below half');
assert.equal(deck.held, 2, '+1 held');
assert.ok(Math.abs(u.skill.sp - spBefore - 3) < 0.2, `T2: +3 SP (${spBefore} → ${u.skill.sp})`);
assert.equal(u.findBuff('ling:t2').stacks, 1, 'T2 stack');
void sp0;
done(h);
}
});
test('S3 宁作吾: she and her summons ATK +55 % / +70 %, DEF +50 % / +60 % for 25 / 30 s; every 0.5 s each summon\'s x-5 ground enemies take 20 % of 令\'s ATK as arts (her damage, not dodgeable; no air, nothing outside); +1 held at its end', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
const sk = skillOf(tier, elite, S3);
const { h, u, ss, deck } = field({ tier, elite, mod, skill: 2, souls: [[J, 10, 6, 'RIGHT']] });
const s = ss[0];
assert.deepEqual([sk.bb.atk, sk.bb.def, sk.bb.atk_scale, sk.bb.interval, sk.bb.cnt, sk.duration], [elite ? 0.7 : 0.55, elite ? 0.6 : 0.5, 0.2, 0.5, 1, elite ? 30 : 25]);
const front = h.spawn('enemy_dummy', { pos: [10, 7] }), side = h.spawn('enemy_dummy', { pos: [11, 6] });
const fly = h.spawn('enemy_fly', { pos: [9, 6] }), diag = h.spawn('enemy_dummy', { pos: [11, 7] });
h.spawn('enemy_dummy', { pos: [10, 4] });
deck.held = 0;
const atk0 = u.s.atk, satk0 = s.s.atk, sdef0 = s.s.def;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
approx(u.s.atk, atk0 * (1 + sk.bb.atk), 'her ATK');
approx(s.s.atk, satk0 * (1 + sk.bb.atk), 'its ATK');
approx(s.s.def, sdef0 * (1 + sk.bb.def), 'its DEF');
h.run(3.05);
const pulses = h.hooksOf('damaged').filter((x) => x.dmg?.tags?.includes('ling:s3'));
assert.ok(pulses.length > 0, 'pulses');
assert.ok(pulses.every((x) => x.source === u), '令\'s damage');
assert.deepEqual(new Set(pulses.map((x) => x.target)), new Set([front, side]), 'its x-5 ground enemies only');
for (const x of pulses) { approx(x.amount, u.s.atk * 0.2, '20 % of her ATK'); assert.deepEqual([x.type, x.dmg.canDodge, x.dmg.isSplash], ['arts', false, true]); }
assert.equal(pulses.filter((x) => x.target === front).length, 6, 'every 0.5 s (6 in 3 s)');
void fly; void diag;
h.runUntil(() => !u.skill.active, 40);
assert.equal(deck.held, 1, '技能结束时获得1个召唤物');
done(h);
}
});
test('the 弦惊 merge: another basic 弦惊 on a new one\'s range ⇒ that one turns 高级形态 and the new one is absorbed (T2); a new one on the range of basic ones ⇒ it turns 高级形态 and the latest of them is absorbed; 高级形态 = HP / ATK / DEF ×(1 + 1 / 0.8 / 0.8), RES ×2, interval +0.8 s, block +2, arts, full HP; never with a 高级形态 one', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
const tb = tokOf(J, tier, elite, 2, mod).skill.bb;
assert.deepEqual(tb, { '2.max_hp': 1, '2.atk': 0.8, '2.def': 0.8, '2.magic_resistance': 1, '2.base_attack_time': 0.8, '2.block_cnt': 2 });
// case 1: B (deployed second) faces A — A on B's range ⇒ A evolves, B absorbed
const { h, u, ss } = field({ tier, elite, mod, skill: 2, souls: [[J, 9, 5, 'RIGHT'], [J, 9, 6, 'LEFT']] });
const [a, b] = ss;
assert.deepEqual([a.alive, a.mem.lingAdvanced, b.alive], [true, true, false], 'case 1');
const base = tokOf(J, tier, elite, 2, mod).stats;
approx(a.s.maxHp, base.maxHp * 2, 'HP ×2');
approx(a.hp, a.s.maxHp, 'full HP');
approx(a.s.atk, base.atk * 1.8, 'ATK ×1.8');
approx(a.s.def, base.def * 1.8, 'DEF ×1.8');
approx(a.s.res, base.res * 2, 'RES ×2');
approx(a.s.interval, base.bat + 0.8, 'interval +0.8 s');
assert.equal(a.s.blockCnt, base.blockCnt + 2, 'block +2');
assert.equal(u.findBuff('ling:t2')?.stacks, 1, 'T2: absorbed');
// its attacks are arts
h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(h.runUntil(() => from(h, a).some((x) => x.dmg?.isAttack), 60), 'it fights');
assert.ok(from(h, a).filter((x) => x.dmg?.isAttack).every((x) => x.type === 'arts'), 'arts');
// the absorbed one comes back basic beside it (no merge with a 高级形态 one)
assert.ok(h.runUntil(() => b.alive, 25), 'the absorbed piece comes back');
assert.deepEqual([b.mem.lingAdvanced, a.mem.lingAdvanced, a.alive], [false, true, true], 'no second merge');
done(h);
// case 2: A faces B and B deploys later — B on A's range ⇒ B evolves, A (the latest such) absorbed
const r2 = field({ tier, elite, mod, skill: 2, souls: [[J, 10, 5, 'RIGHT'], [J, 10, 6, 'RIGHT']] });
const [a2, b2] = r2.ss;
assert.deepEqual([a2.alive, b2.alive, b2.mem.lingAdvanced], [false, true, true], 'case 2');
// not facing each other: no merge
const r3 = field({ tier, elite, mod, skill: 2, souls: [[J, 10, 5, 'LEFT'], [J, 10, 6, 'RIGHT']] });
assert.ok(r3.ss.every((x) => x.alive && !x.mem.lingAdvanced), 'apart: none');
done(r2.h);
done(r3.h);
}
});
test('SUM-Y 诗短梦长: +3 held on the card and cap 8 (5 + 3) at both stages, 4 standing at stage 3; the module\'s talent text is the stage-3 T1', () => {
const { h, deck } = field({ tier: 5, elite: true, mod: SUMY, skill: 0 });
assert.deepEqual([deck.held, deck.cap, deck.maxDeployed], [8, 8, 3], 'stage 1');
done(h);
const { h: h2, deck: d2 } = field({ tier: 6, elite: true, mod: SUMY, skill: 0 });
assert.deepEqual([d2.held, d2.cap, d2.maxDeployed], [8, 8, 4], 'stage 3');
assert.match(modOf(6, SUMY).talentChanges[0].desc, /最多同时部署4个/);
done(h2);
});
+332
View File
@@ -0,0 +1,332 @@
// test/content/op_mgllan.test.js — the 自选 operator kit of 麦哲伦 (char_248_mgllan, 6★ 召唤师; kit
// server/sim/content/kits/ops/op-mgllan.js, the summon deck of kits/shared/summoner.js) and of her drones 龙腾.F / .L / .A,
// fielded the production way (a DIY slot + its `diy` pick, simdata getDiy; the drone 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, SUM-X
// 专业无人机操作模块 or SUM-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_mgllan.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 { canTargetAlly } from '../../server/sim/targeting.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 MG = 'char_248_mgllan';
const FORMS = BACKUPS.units[MG].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const SUMX = 'uniequip_002_mgllan', SUMY = 'uniequip_003_mgllan';
const S1 = 'skchr_mgllan_1', S2 = 'skchr_mgllan_2', S3 = 'skchr_mgllan_3';
const F = 'token_10005_mgllan_drone1', L = 'token_10005_mgllan_drone2', A = 'token_10005_mgllan_drone3';
const DRONE_OF = [F, L, A];
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);
/** A drone's variant of a form, with the pick's skill / module (getDiyToken). */
const tokOf = (tok, tier, elite, skill, mod) => {
let v = BACKUPS.tokens[tok].variants[`${MG}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
return v;
};
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_archer: dummy('enemy_archer', { range: 3, atk: 100 }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, SUMX, SUMY].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 麦哲伦 as uid 1 at (row, col) facing RIGHT; `drones` = [[tokenId, row, col, dir?]] placed pieces (uid 10 +). */
function field({ tier = 5, elite = false, mod = null, skill = 0, drones = [], row = 10, col = 4, seed = 5, dp = 99 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999, dpInit: dp }, captureNoisy: true,
hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack'],
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: MG, skillIndex: skill, uniEquipId: mod }, elite, row, col },
...drones.map(([tokenId, r, c, dir], i) => ({ uid: 10 + i, kind: 'token', tokenId, ownerUid: 1, row: r, col: c, dir: dir ?? 'RIGHT' }))],
});
h.step();
const u = h.unit(1);
return { h, u, ds: h.b.allyUnits.filter((a) => a.kind === 'token'), deck: u.mem.summonDeck };
}
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src && c.dmg?.isAttack);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('麦哲伦 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, ranged arts, hits air, blocks 1, ground-targetable, the data triggers (DEFAULT), 协防, no 特质', () => {
assert.equal(OPERATOR_KITS[MG], KITS[MG]);
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], [MG, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 召唤师`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
assert.deepEqual([u.skill.rule, form.skills[skill].trigger.rule], ['DEFAULT', 'DEFAULT'], `${label(f)}: the data's 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);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 871 / 403 / 116, E2 Lv60 1025 / 447 / 132; SUM-X +100 / +30 → +150 / +50 HP /
// ATK, SUM-Y +25 / +25 → +40 / +40 ATK / DEF
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], [871, 403, 1025, 447]);
assert.deepEqual([modOf(5, SUMX).attr, modOf(6, SUMX).attr, modOf(5, SUMY).attr, modOf(6, SUMY).attr], [{ maxHp: 100, atk: 30 }, { maxHp: 150, atk: 50 }, { atk: 25, def: 25 }, { atk: 40, def: 40 }]);
});
test('a 自选 pick: 麦哲伦 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks with either module', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(MG));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(MG), `tier ${t}`);
for (const uniEquipId of [SUMX, SUMY, null]) {
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: MG, skillIndex: 2, uniEquipId } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: MG, skillIndex: 2, uniEquipId } } });
}
});
test('T1 支援无人机·龙腾: her skill\'s drone is her piece (the others are not hers to field); the deck — 5 held at her deployment, cap 5 (8 with SUM-Y, which adds 3 on the card), at most 3 standing (4 at SUM-Y stage 3); every drone holds 禁疗, blocks nothing, has its data and no skill', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const tok = DRONE_OF[skill];
const { h, u, ds, deck } = field({ tier, elite, mod, skill, drones: [[tok, 11, 6]] });
const d = ds[0], v = tokOf(tok, tier, elite, skill, mod);
const y = elite && mod === SUMY;
assert.ok(h.b.producesToken(u, tok) && DRONE_OF.filter((x) => x !== tok).every((x) => !h.b.producesToken(u, x)), `${label(f)} S${skill + 1}: only ${tok}`);
assert.deepEqual([deck.charge, deck.cap, deck.held, deck.maxDeployed], [5, y ? 8 : 5, y ? 8 : 5, y && tier === 6 ? 4 : 3], `${label(f)} S${skill + 1}: deck`);
assert.deepEqual([d.alive, d.ownerUnit, d.kit.skill, d.skill.noSkill || !d.kit.skill], [true, u, null, true], `${label(f)}: deployed, hers, no skill`);
assert.deepEqual([d.base.maxHp, d.base.atk, d.base.def, d.base.cost, d.base.respawnTime, d.base.bat], [v.stats.maxHp, v.stats.atk, v.stats.def, v.stats.cost, v.stats.respawnTime, v.stats.bat], `${label(f)}: drone stats`);
assert.deepEqual([d.s.blockCnt, !!d.s.flags.noHeal], [0, true], `${label(f)}: no block, 禁疗`);
assert.deepEqual(d.liveRangeGrid, v.rangeGrid, `${label(f)}: drone range`);
if (tok === F) assert.deepEqual([d.profile.noAttack, d.base.atk], [true, 0], '龙腾.F: no attack (ATK 0)');
if (tok === L) assert.deepEqual([d.profile.attack, d.profile.dmgType, d.profile.canHitFly, d.liveRangeGrid], ['melee', 'arts', false, [[0, 0]]], '龙腾.L: melee arts on its tile');
if (tok === A) assert.deepEqual([d.profile.attack, d.profile.dmgType, d.profile.canHitFly, d.profile.splashRadius], ['ranged', 'phys', true, 0.75], '龙腾.A: shells, air too, blast 0.75');
done(h);
}
}
// the SUM-Y drones: F 2 / L 4 / A 5 DP ("部署费用-1 / -3"); stage 3 F 2100 HP / 250 DEF, L ATK 515, A ATK 749, ASPD +3
assert.deepEqual([F, L, A].map((t, i) => tokOf(t, 5, true, i, SUMY).stats.cost), [2, 4, 5]);
assert.deepEqual([tokOf(F, 6, true, 0, SUMY).stats.maxHp, tokOf(F, 6, true, 0, SUMY).stats.def, tokOf(L, 6, true, 1, SUMY).stats.atk, tokOf(A, 6, true, 2, SUMY).stats.atk, tokOf(A, 6, true, 2, SUMY).stats.aspd], [2100, 250, 515, 749, 103]);
});
test('the deck: a knocked-out drone comes back on its tile its redeploy time (10 s) later, paying its cost and using one held; with none held it stays off; the drones leave with her and come back after her next deployment (+5 held)', () => {
const { h, u, ds, deck } = field({ tier: 6, elite: true, mod: SUMX, skill: 2, drones: [[A, 11, 6], [A, 9, 6]] });
const [d1, d2] = ds;
const p = h.b.players[0];
h.b.kill(d1, null);
assert.deepEqual([d1.alive, d1.removed], [false, false], 'the piece stays');
h.run(9.5);
assert.equal(d1.alive, false, 'not before 10 s');
const dp0 = p.dp;
assert.ok(h.runUntil(() => d1.alive, 1), 'back after 10 s');
assert.deepEqual([d1.tileR, d1.tileC, deck.held], [11, 6, 4], 'on its tile, one held used');
approx(dp0 - p.dp, d1.base.cost, 'its cost paid');
// none held: it stays off
deck.held = 0;
h.b.kill(d1, null);
h.run(15);
assert.equal(d1.alive, false, 'none held');
deck.held = 2;
assert.ok(h.runUntil(() => d1.alive, 1), 'back once one is held');
// she leaves: her drones go with her (no knock-down); back after her redeployment
h.b.retreat(u);
h.step();
assert.deepEqual([d1.alive, d2.alive], [false, false], 'withdrawn with her');
assert.ok(h.hooksOf('death').filter((c) => c.unit === d1 || c.unit === d2).slice(-2).every((c) => c.reason === 'retreat'), 'withdrawn, not knocked out');
h.run(20);
assert.deepEqual([d1.alive, d2.alive], [false, false], 'never while she is off the field');
deck.held = 0;
assert.ok(h.b.redeploy(u), 'she comes back');
assert.ok(h.runUntil(() => d1.alive && d2.alive, 1), 'her drones follow');
assert.equal(deck.held, 3, '+5 at her deployment, two used');
done(h);
});
test('T2 光学折射配件: a drone is 隐匿 for 20 s (26 s with SUM-X stage 3) from each deployment — no ranged enemy targets it meanwhile; SUM-X stage 3: 麦哲伦 beside a drone inside that window is 隐匿 too (not two tiles away, not at stage 1)', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, SUMX], [6, true, SUMX], [6, true, SUMY]]) {
const x3 = elite && mod === SUMX && tier === 6;
const want = x3 ? 26 : 20;
const { h, u, ds } = field({ tier, elite, mod, skill: 1, drones: [[L, 10, 5], [L, 11, 7]] });
const [near, far] = ds;
assert.equal(tokOf(L, tier, elite, 1, mod).talents.find((t) => t.bb.hidden_duration != null).bb.hidden_duration, want, `${label([tier, elite, mod])}: data`);
const archer = h.spawn('enemy_archer', { pos: [11, 9] });
assert.ok(near.s.flags.stealth && far.s.flags.stealth, '隐匿 at deployment');
assert.ok(!canTargetAlly(archer, far, true), 'a ranged enemy cannot target it');
assert.equal(!!u.s.flags.stealth, x3, `${label([tier, elite, mod])}: her own 隐匿 beside the drone`);
h.run(want - 0.5);
assert.ok(near.s.flags.stealth, 'still 隐匿');
assert.equal(!!u.s.flags.stealth, x3, 'hers still');
h.run(1);
assert.ok(!near.s.flags.stealth && !far.s.flags.stealth, `over after ${want} s`);
assert.ok(canTargetAlly(archer, far, true), 'targetable again');
assert.ok(!u.s.flags.stealth, 'hers over with it');
// a new deployment: a new window
h.b.kill(far, null);
h.runUntil(() => far.alive, 12);
assert.ok(far.s.flags.stealth, 'again at its next deployment');
done(h);
}
// her 隐匿 needs the drone on one of her four neighbouring tiles
const { h, u } = field({ tier: 6, elite: true, mod: SUMX, skill: 1, drones: [[L, 10, 6]] });
assert.ok(!u.s.flags.stealth, 'two tiles away: no');
done(h);
});
test('S1 高效制冷模块: passive — every 3 s from her deployment 停顿 0.8 / 0.9 s on every enemy of her 3-1 and her 龙腾.F\'s x-4 (air units too), none elsewhere; running (15 s) — 束缚 1.9 / 2.2 s every 3 s from the cast instead; it ends recalling her drones', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
const sk = skillOf(tier, elite, S1);
const { h, u, ds, deck } = field({ tier, elite, mod, skill: 0, row: 10, col: 3, drones: [[F, 11, 8]] });
const d = ds[0];
assert.deepEqual([sk.bb['attack@interval'], sk.bb['attack@sluggish'], sk.bb['attack@frozen_duration'], sk.duration], [3, elite ? 0.9 : 0.8, elite ? 2.2 : 1.9, 15]);
const at = (pos, key = 'enemy_dummy') => h.spawn(key, { pos });
const her = at([10, 5]), fly = at([11, 4], 'enemy_fly'), droneE = at([12, 9]), out = at([9, 9]);
const st = (status) => h.hooksOf('statusApplied').filter((c) => c.status === status);
h.run(2.9);
assert.equal(st('sluggish').length, 0, 'nothing before 3 s');
h.run(0.2);
const first = st('sluggish');
assert.deepEqual(new Set(first.map((c) => c.target)), new Set([her, fly, droneE]), 'her range + the drone\'s x-4');
for (const c of first) approx(c.duration, sk.bb['attack@sluggish'], 'duration');
assert.equal(first.find((c) => c.target === droneE).source, d, 'the drone\'s own area');
assert.ok(!first.some((c) => c.target === out), 'outside: untouched');
h.run(3);
assert.equal(st('sluggish').length, 6, 'again 3 s later');
// the cast: 束缚 every 3 s instead
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast on an attack');
const t0 = h.b.time, n0 = st('sluggish').length;
h.run(2.9);
assert.equal(st('bind').length, 0, 'no 束缚 before 3 s into it');
h.run(0.2);
assert.deepEqual(new Set(st('bind').map((c) => c.target)), new Set([her, fly, droneE]), '束缚 on the same areas');
for (const c of st('bind')) approx(c.duration, sk.bb['attack@frozen_duration'], '束缚 duration');
h.runUntil(() => !u.skill.active, 15);
assert.equal(st('sluggish').length, n0, 'no 停顿 while it runs');
approx(h.b.time - t0, 15, '15 s', 0.01);
// 技能结束时回收所有无人机
h.step();
assert.deepEqual([d.alive, d.removed, deck.held], [false, false, deck.cap], 'recalled (+1 held, at most the cap)');
assert.ok(h.runUntil(() => d.alive, 10.5), 'back on its tile 10 s later');
done(h);
}
});
test('S2 激光开采模块: she and her drones ASPD +80 / +100 for 15 s; a 龙腾.L strikes every enemy on its tile while it runs (one at a time otherwise); recalled at the end', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMX]]) {
const sk = skillOf(tier, elite, S2);
const { h, u, ds } = field({ tier, elite, mod, skill: 1, row: 10, col: 3, drones: [[L, 10, 6]] });
const d = ds[0];
const a = h.spawn('enemy_dummy', { pos: [10, 6] }), b = h.spawn('enemy_dummy', { pos: [10, 6] });
h.run(3);
const before = from(h, d);
assert.ok(before.length >= 2, 'it attacks');
assert.ok(new Set(before.map((c) => c.dmg.attackId)).size === before.length, 'one target per attack');
for (const c of before) assert.deepEqual([c.type, Math.abs(c.amount - Math.max(d.s.atk * 0.05, d.s.atk * (1 - c.target.s.res / 100))) < 1e-6], ['arts', true], 'arts');
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
assert.deepEqual(d.findBuff('mgllan:s2:drone')?.mods, { aspd: sk.bb.attack_speed }, 'the drone\'s bonus');
assert.equal(u.s.aspd, 100 + sk.bb.attack_speed, 'her ASPD');
assert.equal(d.s.aspd, 100 + sk.bb.attack_speed, 'the drone\'s');
const n0 = from(h, d).length;
h.run(3);
const during = from(h, d).slice(n0);
const byAttack = new Map();
for (const c of during) byAttack.set(c.dmg.attackId, [...(byAttack.get(c.dmg.attackId) ?? []), c.target]);
assert.ok([...byAttack.values()].every((ts) => ts.length === 2 && ts.includes(a) && ts.includes(b)), '群体: both enemies of its tile each attack');
// knocked out during it: back 10 s later, still inside the 15 s — with the skill's ASPD
h.b.kill(d, null);
assert.ok(h.runUntil(() => d.alive, 10.5) && u.skill.active, 'back while it runs');
assert.equal(d.s.aspd, 100 + sk.bb.attack_speed, 'a drone deployed during it takes its ASPD');
h.runUntil(() => !u.skill.active, 15);
h.step();
assert.deepEqual([d.alive, u.s.aspd], [false, 100], 'recalled; her ASPD back');
assert.ok(h.runUntil(() => d.alive, 10.5), 'back');
assert.equal(d.findBuff('mgllan:s2:drone'), null, 'no bonus after it');
done(h);
}
});
test('S3 武装打击模块: she and her drones ATK +80 % / +100 % for 15 s; a 龙腾.A\'s blast radius 0.75 → 1.25 while it runs; recalled at the end', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SUMY]]) {
const sk = skillOf(tier, elite, S3);
const { h, u, ds } = field({ tier, elite, mod, skill: 2, row: 10, col: 3, drones: [[A, 10, 5]] });
const d = ds[0];
const main = h.spawn('enemy_dummy', { pos: [10, 7] }), side = h.spawn('enemy_dummy', { pos: [10, 8] });
const close = h.spawn('enemy_fly', { pos: [10.6, 7] });
h.spawn('enemy_dummy', { pos: [11, 4] }); // hers (outside the drone's range)
h.run(5);
const before = from(h, d);
assert.ok(before.some((c) => c.target === main) && before.some((c) => c.target === close), 'its target and a flyer 0.6 away');
assert.ok(!before.some((c) => c.target === side), 'one 1.0 away: outside 0.75');
for (const c of before.filter((x) => x.target === main)) approx(c.amount, d.s.atk, 'phys (DEF 0)');
const atk0 = u.s.atk, datk0 = d.s.atk;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
approx(u.s.atk, atk0 * (1 + sk.bb.atk) / 1, 'her ATK +', 1e-9);
approx(d.s.atk, datk0 * (1 + sk.bb.atk), 'the drone\'s ATK +');
assert.equal(d.profile.splashRadius, 1.25, 'blast 1.25');
const n0 = from(h, d).length;
h.run(5);
assert.ok(from(h, d).slice(n0).some((c) => c.target === side), 'now 1.0 away too');
h.runUntil(() => !u.skill.active, 15);
h.step();
assert.equal(d.alive, false, 'recalled');
assert.ok(h.runUntil(() => d.alive, 10.5), 'back');
assert.deepEqual([d.profile.splashRadius, d.s.atk], [0.75, datk0], 'blast and ATK back');
done(h);
}
});
test('the recall triggers each drone\'s own skill: a stunned or silenced drone stays (PRTS 备注 "技能无法触发的场合不会产生效果"); none when she leaves the field with the skill running (her drones go with her)', () => {
const { h, u, ds } = field({ tier: 6, elite: true, mod: SUMX, skill: 2, row: 10, col: 3, drones: [[A, 10, 5], [A, 11, 5], [A, 9, 5]] });
const [ok, stunned, silenced] = ds;
h.spawn('enemy_dummy', { pos: [10, 5] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
u.skill.timeLeft = 0.2;
h.b.applyStatus(stunned, 'stun', { duration: 5, force: true });
h.b.applyStatus(silenced, 'silence', { duration: 5, force: true });
h.runUntil(() => !u.skill.active, 1);
h.step();
assert.deepEqual([ok.alive, stunned.alive, silenced.alive], [false, true, true], 'only the free one');
// her leaving with the skill on: withdrawn with her, no recall (no +1 held)
const { h: h2, u: u2, ds: ds2, deck } = field({ tier: 5, elite: false, skill: 2, row: 10, col: 3, drones: [[A, 10, 5]] });
h2.spawn('enemy_dummy', { pos: [10, 5] });
u2.skill.gainSp(999);
h2.runUntil(() => u2.skill.active, 3);
deck.held = 2;
h2.b.kill(u2, null);
h2.step();
assert.deepEqual([ds2[0].alive, deck.held], [false, 2], 'gone with her, nothing held more');
done(h);
done(h2);
});
test('modules: SUM-X\'s trait (首个召唤物部署时不消耗部署位) is a prep deploy-cap rule — the deck is the same as without it; SUM-Y\'s +3 held on the card and cap 8 hold through her redeployment (+5, at most 8)', () => {
const { h, deck } = field({ tier: 6, elite: true, mod: SUMX, skill: 0 });
assert.deepEqual([deck.held, deck.cap, deck.maxDeployed], [5, 5, 3], 'SUM-X: the base deck');
assert.equal(formOf(6, true).modules.find((m) => m.uniEquipId === SUMX).traitOverride.bb.cnt, 1, 'its trait blackboard (unused)');
done(h);
const { h: h2, u, deck: d2 } = field({ tier: 5, elite: true, mod: SUMY, skill: 0 });
assert.deepEqual([d2.held, d2.cap, d2.maxDeployed], [8, 8, 3], 'SUM-Y stage 1: 3 + 5 held, cap 8, 3 standing');
d2.held = 1;
h2.b.retreat(u);
h2.b.redeploy(u);
assert.equal(d2.held, 6, '+5 at her next deployment');
h2.b.retreat(u);
h2.b.redeploy(u);
assert.equal(d2.held, 8, 'at most 8');
done(h2);
});
+291
View File
@@ -0,0 +1,291 @@
// test/content/op_radian.test.js — the 自选 operator kit of 电弧 (char_4195_radian, 6★ 召唤师; kit
// server/sim/content/kits/ops/op-radian.js, the summon deck of kits/shared/summoner.js) and of her summons 戴乌 / 赛柯 /
// 桑特拉, fielded the production way (a DIY slot + its `diy` pick, simdata getDiy; the summons as the placed hand pieces 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,
// SO-A 电弧特勤证章 or SO-B 新起点 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_radian.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 RAD = 'char_4195_radian';
const FORMS = BACKUPS.units[RAD].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const SOA = 'uniequip_002_radian', SOB = 'uniequip_003_radian';
const S1 = 'skchr_radian_1', S2 = 'skchr_radian_2', S3 = 'skchr_radian_3';
const DIVE = 'token_10051_radian_tower1', CYCLE = 'token_10052_radian_tower2', CENTRE = 'token_10053_radian_tower3';
const TOWER_OF = [DIVE, CYCLE, CENTRE];
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 tokOf = (tok, tier, elite, skill, mod) => {
let v = BACKUPS.tokens[tok].variants[`${RAD}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
return v;
};
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' }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, SOA, SOB].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 电弧 as uid 1 at (row, col) facing RIGHT; `towers` = [[tokenId, row, col, dir?]] placed pieces (uid 10 +). */
function field({ tier = 5, elite = false, mod = null, skill = 0, towers = [], row = 10, col = 3, seed = 5, dp = 99 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999, dpInit: dp }, captureNoisy: true,
hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack'],
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: RAD, skillIndex: skill, uniEquipId: mod }, elite, row, col },
...towers.map(([tokenId, r, c, dir], i) => ({ uid: 10 + i, kind: 'token', tokenId, ownerUid: 1, row: r, col: c, dir: dir ?? 'RIGHT' }))],
});
h.step();
const u = h.unit(1);
return { h, u, ts: h.b.allyUnits.filter((a) => a.kind === 'token'), deck: u.mem.summonDeck };
}
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('电弧 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, ranged arts, hits air, blocks 1, ground-targetable, the data triggers (DEFAULT), 协防, no 特质', () => {
assert.equal(OPERATOR_KITS[RAD], KITS[RAD]);
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], [RAD, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.profile.maxTargets, u.base.bat], [1, 'ranged', 'arts', true, 1, 1.6], `${label(f)}: 召唤师 (SO-B's 额外攻击一个目标 is 集成战略-only)`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
assert.deepEqual([u.skill.rule, form.skills[skill].trigger.rule], ['DEFAULT', 'DEFAULT'], `${label(f)}: the data's 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);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 844 / 400 / 118, E2 Lv60 993 / 444 / 135; SO-A +125 / +27 → +165 / +43 HP / ATK,
// SO-B +41 → +65 ATK
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], [844, 400, 993, 444]);
assert.deepEqual([modOf(5, SOA).attr, modOf(6, SOA).attr, modOf(5, SOB).attr, modOf(6, SOB).attr], [{ maxHp: 125, atk: 27 }, { maxHp: 165, atk: 43 }, { atk: 41 }, { atk: 65 }]);
});
test('a 自选 pick: 电弧 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks (the 岁的界园志异 modules SO-A / SO-B are not ISW-*)', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(RAD));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(RAD), `tier ${t}`);
for (const uniEquipId of [SOA, SOB, null]) {
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: RAD, skillIndex: 0, uniEquipId } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: RAD, skillIndex: 0, uniEquipId } } });
}
});
test('T1 卡带里的灵感: her skill\'s summon is her piece; the deck — 5 held, cap 5, at most 3 standing; every summon holds 禁疗 and fights with its data (戴乌 0-1 melee physical blocks 3, 赛柯 blocks 2 and fires straight ahead, 桑特拉 3-1 arts hits air)', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const tok = TOWER_OF[skill];
const { h, u, ts, deck } = field({ tier, elite, mod, skill, towers: [[tok, 10, 6]] });
const t = ts[0], v = tokOf(tok, tier, elite, skill, mod);
assert.ok(h.b.producesToken(u, tok) && TOWER_OF.filter((x) => x !== tok).every((x) => !h.b.producesToken(u, x)), `${label(f)} S${skill + 1}: only ${tok}`);
assert.deepEqual([deck.charge, deck.cap, deck.held, deck.maxDeployed], [5, 5, 5, 3], `${label(f)} S${skill + 1}: deck`);
assert.deepEqual([t.base.maxHp, t.base.atk, t.base.def, t.base.res, t.base.cost, t.base.respawnTime, t.base.bat], [v.stats.maxHp, v.stats.atk, v.stats.def, v.stats.res, v.stats.cost, v.stats.respawnTime, v.stats.bat], `${label(f)}: summon stats (no module change)`);
assert.deepEqual([t.alive, !!t.s.flags.noHeal, t.kit.skill], [true, true, null], `${label(f)}: deployed, 禁疗, no skill`);
const want = { [DIVE]: [3, 'melee', 'phys', false, false], [CYCLE]: [2, 'melee', 'phys', false, true], [CENTRE]: [1, 'ranged', 'arts', true, false] }[tok];
assert.deepEqual([t.s.blockCnt, t.profile.attack, t.profile.dmgType, t.profile.canHitFly, !!t.profile.noAttack], want, `${label(f)}: ${tok}`);
done(h);
}
}
});
test('T2 加油~: her summons on the field get 鼓舞 = 12 % of HER current ATK / DEF / max HP (PRTS 修正 电弧自身), added after their own multipliers, refreshed every second; the strongest 鼓舞 of a kind is kept', () => {
for (const f of [[5, false, null], [6, true, SOA]]) {
const [tier, elite, mod] = f;
const { h, u, ts } = field({ tier, elite, mod, skill: 0, towers: [[DIVE, 10, 6]] });
const t = ts[0];
assert.deepEqual(formOf(tier, elite).talents.find((x) => x.index === 1).bb, { max_hp: 0.12, def: 0.12, atk: 0.12 });
approx(t.s.atk, t.base.atk + 0.12 * u.s.atk, `${label(f)}: ATK`);
approx(t.s.def, t.base.def + 0.12 * u.s.def, `${label(f)}: DEF`);
approx(t.s.maxHp, t.base.maxHp + 0.12 * u.s.maxHp, `${label(f)}: max HP`);
// her ATK rises: the 鼓舞 follows within a second
h.b.addBuff(u, { key: 'test:atk', mods: { atkPct: 1 } });
h.run(1.05);
approx(t.s.atk, t.base.atk + 0.12 * u.s.atk, 'updated every second');
// a summon's own ATK +% does not scale it
h.b.addBuff(t, { key: 'test:tatk', mods: { atkPct: 0.5 } });
h.run(1.05);
approx(t.s.atk, t.base.atk * 1.5 + 0.12 * u.s.atk, 'after its multipliers');
// a stronger 鼓舞 of another source stays
h.b.addBuff(t, { key: 'inspire', mods: { atkFlat: 5000 }, duration: 30, data: { src: -1, val: 5000 } });
h.run(1.05);
assert.equal(t.findBuff('inspire').data.val, 5000, 'the strongest kept');
done(h);
}
});
test('S1 律动线: +1 held; she and her summons DEF +40 % / +60 % and a 屏障 of 30 % / 70 % of their max HP until it ends (25 s; it stops every type but 元素伤害); a summon deployed during it takes it at its deployment with its 鼓舞 HP', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SOB]]) {
const sk = skillOf(tier, elite, S1);
const { h, u, ts, deck } = field({ tier, elite, mod, skill: 0, towers: [[DIVE, 10, 6], [DIVE, 11, 6]] });
const [t, late] = ts;
assert.deepEqual([sk.bb.hp_ratio, sk.bb.def, sk.bb.cnt, sk.duration], [elite ? 0.7 : 0.3, elite ? 0.6 : 0.4, 1, 25]);
h.b.kill(late, null);
h.spawn('enemy_dummy', { pos: [10, 4] });
deck.held = 0;
const def0 = u.s.def;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
assert.equal(deck.held, 1, '技能开启时获得1个召唤物');
approx(u.s.def, def0 * (1 + sk.bb.def), 'her DEF');
assert.deepEqual(t.findBuff('radian:s1:tower')?.mods, { defPct: sk.bb.def }, 'its DEF');
const bu = u.findBuff('radian:s1:barrier'), bt = t.findBuff('radian:s1:barrier');
approx(bu.shield, u.s.maxHp * sk.bb.hp_ratio, 'her 屏障');
approx(bt.shield, t.s.maxHp * sk.bb.hp_ratio, 'its 屏障');
assert.deepEqual(bt.shieldType, ['phys', 'arts', 'true'], 'not 元素伤害');
// the knocked-out one comes back during the skill: its 屏障 counts its 鼓舞 HP
assert.ok(h.runUntil(() => late.alive, 21), 'back during the skill');
approx(late.findBuff('radian:s1:barrier').shield, (late.base.maxHp + 0.12 * u.s.maxHp) * sk.bb.hp_ratio, 'with 鼓舞 HP');
h.runUntil(() => !u.skill.active, 30);
assert.ok([u, t, late].every((x) => !x.findBuff('radian:s1:barrier')), 'gone at its end');
assert.equal(t.findBuff('radian:s1:tower'), null, 'DEF back');
done(h);
}
});
test('赛柯 (S2\'s summon): picks no target — every attack interval it fires a bullet straight ahead (5 tiles/s, 0.6 s, radius 0.5) at the first ground enemy it touches (100 % ATK physical 普通伤害), none at air units or farther than 3 tiles; stunned: no shot', () => {
const { h, u, ts } = field({ tier: 5, elite: false, skill: 1, towers: [[CYCLE, 10, 5]] });
const t = ts[0];
const near = h.spawn('enemy_dummy', { pos: [10, 7] }), behind = h.spawn('enemy_dummy', { pos: [10, 7.4] });
const fly = h.spawn('enemy_fly', { pos: [10, 6] }), far = h.spawn('enemy_dummy', { pos: [11, 9] });
h.run(4);
const hits = from(h, t);
assert.equal(hits.length, 3, '3 shots landed in 4 s (one at its deployment, then every 1.8 s; 0.3 s of flight)');
assert.ok(hits.every((c) => c.target === near), 'the first ground enemy ahead only');
for (const c of hits) assert.deepEqual([c.type, c.dmg.isAttack, Math.abs(c.amount - t.s.atk) < 1e-6], ['phys', true, true]);
void behind; void fly; void far;
// nothing past its 3 tiles
near.hp = 0; h.b.kill(near, null); behind.hp = 0; h.b.kill(behind, null);
const out = h.spawn('enemy_dummy', { pos: [10, 8.6] });
const n0 = from(h, t).length;
h.run(4);
assert.equal(from(h, t).length, n0, 'out of reach (3 tiles of flight + 0.5 radius)');
void out;
// stunned: no shot
const close = h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.applyStatus(t, 'stun', { duration: 5, force: true });
const n1 = from(h, t).length;
h.run(4);
assert.equal(from(h, t).length, n1, 'no shot while stunned');
h.run(3);
assert.ok(from(h, t).some((c) => c.target === close), 'shoots again');
void u;
done(h);
});
test('S2 环形鳞地: +1 held; she and her summons ATK +35 % / +50 % for 25 s; 赛柯 fires three bullets per attack that fly 0.6 / 0.8 s (rank 4 / 7: "子弹飞行距离+1")', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SOA]]) {
const sk = skillOf(tier, elite, S2);
const life = tokOf(CYCLE, tier, elite, 1, mod).skill.bb['attack@projectile_life_time'];
assert.deepEqual([sk.bb.atk, sk.bb.base_attack_time, life], [elite ? 0.5 : 0.35, 0, elite ? 0.8 : 0.6]);
const { h, u, ts, deck } = field({ tier, elite, mod, skill: 1, towers: [[CYCLE, 10, 5]] });
const t = ts[0];
h.spawn('enemy_dummy', { pos: [10, 4] });
const reach = h.spawn('enemy_dummy', { pos: [10, 5 + 3.8] }); // 3.8 tiles ahead: past 0.6 s + 0.5, inside 0.8 s + 0.5
deck.held = 0;
const atk0 = u.s.atk, tatk0 = t.s.atk;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
assert.equal(deck.held, 1, '+1 held');
approx(u.s.atk, atk0 * (1 + sk.bb.atk), 'her ATK');
approx(t.s.atk, tatk0 * (1 + sk.bb.atk), 'its ATK');
const n0 = from(h, t).length;
h.run(3.7);
const hits = from(h, t).slice(n0);
if (elite) {
assert.ok(hits.length >= 3 && hits.length % 3 === 0, `three bullets per attack: ${hits.length}`);
assert.ok(hits.every((c) => c.target === reach), 'they reach 3.8 tiles');
} else assert.equal(hits.length, 0, 'rank 4: 0.6 s — 3.8 tiles is out of reach');
done(h);
}
});
test('S3 手牵手: +1 held; she and her summons ATK +80 % / +110 % for 30 s; before each damage of her attacks and of her summons on an enemy: 停顿 2 s and 法术脆弱 10 % / 20 % 2 s', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, SOB]]) {
const sk = skillOf(tier, elite, S3);
const { h, u, ts, deck } = field({ tier, elite, mod, skill: 2, towers: [[CENTRE, 11, 5]] });
const t = ts[0];
assert.deepEqual([sk.bb.atk, sk.bb.sluggish, sk.bb['weak[magic][limit]'], sk.bb.damage_scale, sk.duration], [elite ? 1.1 : 0.8, 2, 2, elite ? 1.2 : 1.1, 30]);
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
h.run(3);
assert.equal(h.hooksOf('statusApplied').filter((c) => c.status === 'artsFragile').length, 0, 'nothing before it');
deck.held = 0;
const atk0 = u.s.atk, tatk0 = t.s.atk;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
assert.equal(deck.held, 1, '+1 held');
approx(u.s.atk, atk0 * (1 + sk.bb.atk), 'her ATK');
approx(t.s.atk, tatk0 * (1 + sk.bb.atk), 'its ATK');
h.run(3);
const st = h.hooksOf('statusApplied').filter((c) => c.target === e);
assert.ok(st.some((c) => c.status === 'sluggish' && c.source === u) && st.some((c) => c.status === 'sluggish' && c.source === t), '停顿 from both');
const frag = st.filter((c) => c.status === 'artsFragile');
assert.ok(frag.length && frag.every((c) => Math.abs(c.value - (sk.bb.damage_scale - 1)) < 1e-9 && Math.abs(c.duration - 2) < 1e-9), '法术脆弱');
// before the damage: a hit of hers right after it is ×(1 + fragile)
const hit = from(h, u).filter((c) => c.dmg.isAttack).at(-1);
approx(hit.amount, u.s.atk * (1 + sk.bb.damage_scale - 1), 'her hit carries it (RES 0)');
done(h);
}
});
test('桑特拉 协同攻击: when one attacks, another of the field with no target of its own, its attack ready and the first inside its range shoots a link that strikes the first one\'s target (100 % of ITS ATK, arts), then asks the others; not one with a target, nor one out of range', () => {
// a sees e; b has a in its range; c has b (not a) in its range — it answers b's request in turn; d sees none of them
const { h, ts } = field({ tier: 6, elite: true, skill: 2, towers: [[CENTRE, 10, 5], [CENTRE, 11, 5], [CENTRE, 12, 3], [CENTRE, 12, 9]] });
const [a, b, c, d] = ts;
const e = h.spawn('enemy_dummy', { pos: [10, 8] }); // a's range only
h.run(4);
const links = h.hooksOf('damaged').filter((x) => x.dmg?.tags?.includes('radian:link'));
assert.ok(links.length >= 4, `links: ${links.length}`);
assert.ok(links.every((x) => x.target === e), 'the first one\'s target');
assert.ok(links.some((x) => x.source === b), 'b (a inside its range) answers');
assert.ok(links.some((x) => x.source === c), 'c answers b\'s request (the chain)');
for (const x of links) assert.deepEqual([x.type, x.dmg.isAttack, Math.abs(x.amount - x.source.s.atk) < 1e-6], ['arts', true, true], 'its own ATK, arts');
assert.ok(!links.some((x) => x.source === d), 'd (none of them in its range) does not');
const nb = links.filter((x) => x.source === b).length, na = h.hooksOf('attack').filter((x) => x.attacker === a).length;
assert.ok(nb <= na && nb >= na - 1, `at most one link per attack of a (b answers when its own attack is ready): ${nb} / ${na}`);
// b with a target of its own attacks that one instead
const mine = h.spawn('enemy_dummy', { pos: [12, 6] });
const n0 = links.length;
h.run(4);
const after = h.hooksOf('damaged').filter((x) => x.dmg?.tags?.includes('radian:link')).slice(n0);
assert.ok(!after.some((x) => x.source === b), 'b busy with its own target');
assert.ok(from(h, b).some((x) => x.target === mine && !x.dmg.tags?.includes('radian:link')), 'b attacks it');
done(h);
});
test('modules SO-A / SO-B: only their attributes act — the 集成战略 parts the record carries (SO-A\'s respawn_time / sp, SO-B\'s attack@max_target and the x-5 recall) change nothing: same deck, one target, the summons\' redeploy time and no SP when one leaves', () => {
for (const mod of [SOA, SOB]) {
const { h, u, ts, deck } = field({ tier: 6, elite: true, mod, skill: 0, towers: [[DIVE, 10, 4]] });
const t = ts[0];
assert.ok(u.def.raw.talents.some((x) => x.index === -1), `${mod}: the record carries its hidden 集成战略 talent`);
assert.deepEqual([deck.charge, deck.cap, deck.maxDeployed, u.profile.maxTargets, u.s.maxTargets], [5, 5, 3, 1, 0], `${mod}: nothing`);
assert.ok(t.alive, `${mod}: the summon beside her stays (no x-5 recall)`);
const sp0 = u.skill.sp;
h.b.kill(t, null);
assert.equal(u.skill.sp, sp0, `${mod}: no SP`);
h.run(19.5);
assert.equal(t.alive, false, `${mod}: its full redeploy time (20 s)`);
assert.ok(h.runUntil(() => t.alive, 1), `${mod}: back after 20 s`);
done(h);
}
});
+347 -96
View File
@@ -4495,8 +4495,8 @@
"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=mgllan/S1 5_diy2_a=mgllan/S2 5_diy1_a=mgllan/S3 5_diy2_b=mgllan/S1 5_diy1_b=mgllan/S2 5_diy2_b=mgllan/S3 5_diy1_b=mgllan/S1/002_mgllan 5_diy2_b=mgllan/S2/002_mgllan 5_diy1_b=mgllan/S3/002_mgllan 5_diy2_b=mgllan/S1/003_mgllan 5_diy1_b=mgllan/S2/003_mgllan 5_diy2_b=mgllan/S3/003_mgllan",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_abyss",
@@ -4512,111 +4512,111 @@
"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":0,"total":126,"perfect":false,"leaks":126,"leaksUncounted":0,"coins":0,"dmg":31933,"bossDmg":0,"heal":0,"deaths":28,"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,0,0,156,0,2418,2613,0,12,1],
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,150,0,2620,2613,0,13,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,92,0,2176,1742,0,10,1],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,81,0,2682,2050,0,12,1],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,143,0,1788,3075,0,8,0],
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,112,0,4193,2050,0,13,1],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_mgllan",0,0,124,0,2385,2250,0,10,1],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_mgllan",0,0,90,0,3492,2250,0,15,1],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_mgllan",0,0,85,0,3015,2250,0,12,1],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_mgllan",0,0,140,0,2209,3075,0,10,0],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_mgllan",0,0,88,0,2832,2050,0,12,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_mgllan",0,0,80,0,2124,2050,0,9,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]
],
"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},
"fx": {},
"snaps": "57186c39 a89f274b e649d7d9 75621917 006f6ba9 7f3b8dda bb3864de e8193348 16cc7f17 db8194aa e07167eb 0946e952 5d27c316 103f7135 f7e98be4 ef855c6a d34a4bb6 5d39faae c6e66696 77bde8df"
"enemies": {"enemy_1251_lysyta_2":[54,0,54,2050,16427],"enemy_1252_lysytb_2":[42,0,42,26018,8905],"enemy_1329_cbshld_2":[30,0,30,0,6601]},
"hooks": [1,156,5814,199,199,194,194,0,28,154,9,9,0,7064,32,4,126,126,1,28,0,0,0],
"events": {"atk":199,"deploy":28,"die":28,"dmg":194,"fx":27,"leak":126,"skill":18,"spawn":140,"status":56},
"fx": {"coldWind":27},
"snaps": "104215d6 2d86448a e83521b7 2f3b8f2c 467aaa33 36504f37 4f52ff7a cdfa8c88 caeb9cf4 8937b77d 75d79154 537c6c37 a7aadd9d 128d982b 8e2c9846 5b6304e6 6eeb26e8 084ea0b4 690a5020 6298d790"
},
"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=mgllan/S1 6_diy2_a=mgllan/S2 6_diy1_a=mgllan/S3 6_diy2_b=mgllan/S1 6_diy1_b=mgllan/S2 6_diy2_b=mgllan/S3 6_diy1_b=mgllan/S1/002_mgllan 6_diy2_b=mgllan/S2/002_mgllan 6_diy1_b=mgllan/S3/002_mgllan 6_diy2_b=mgllan/S1/003_mgllan 6_diy1_b=mgllan/S2/003_mgllan 6_diy2_b=mgllan/S3/003_mgllan",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_multi_funny",
"round": 12,
"round": 13,
"seed": 4020340196,
"waveId": "act1autochess_h05",
"time": 203.3,
"ticks": 6099,
"waveId": "act1autochess_h06",
"time": 219.8,
"ticks": 6594,
"reason": "cleared",
"killed": 90,
"total": 90,
"killed": 22,
"total": 96,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 450,
"rngDraws": 0,
"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":{}}},
"players": {"p1":{"killed":22,"total":96,"perfect":false,"leaks":74,"leaksUncounted":0,"coins":0,"dmg":193618,"bossDmg":0,"heal":0,"deaths":23,"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]
["p1",1,"op","chess_char_6_diy1_a",0,null,871,1,2,1,27591,0,0,115,4],
["p1",2,"op","chess_char_6_diy2_a",1,null,871,1,3,1,34408,0,0,143,4],
["p1",3,"op","chess_char_6_diy1_a",2,null,871,1,4,1,35737,0,0,120,4],
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,122,4,8050,3075,0,33,1],
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,121,0,11533,3075,0,43,1],
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,117,2,11153,3075,0,33,1],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_mgllan",0,0,97,1,4909,2350,0,24,1],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_mgllan",0,0,119,3,12700,3525,0,44,1],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_mgllan",0,0,99,2,16721,2350,0,47,1],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_mgllan",0,0,111,1,7228,3075,0,25,1],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_mgllan",0,0,104,1,7610,2050,0,27,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_mgllan",0,0,100,5,15978,2050,0,48,1]
],
"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"
"enemies": {"enemy_1229_darmy":[27,7,20,2233,47205],"enemy_1229_darmy_2":[57,15,42,17363,134023],"enemy_9007_acelem":[12,0,12,5029,12390]},
"hooks": [1,122,6594,785,785,828,895,0,45,119,21,19,0,26840,666,36,96,74,1,45,0,0,0],
"events": {"atk":785,"deploy":26,"die":45,"dmg":895,"fx":136,"leak":74,"skill":40,"spawn":108,"status":1217},
"fx": {"beam":7,"coldWind":129},
"snaps": "e09e4ac9 e6027628 1120d251 55805310 396237b2 1cfa1c24 01ebd33b 4bd7e803 13e5e373 f3e0bd7d 071f4883 a72d77c5 3c1e1250 1f202fe5 50fdc263 b60c77e1 dc950c29 9e5fc057 65ee6b80 4350b8ce 6d6b3aab 80a7740a"
},
"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",
"about": "5_diy1_a=ling/S1 5_diy2_a=ling/S2 5_diy1_a=ling/S3 5_diy2_b=ling/S1 5_diy1_b=ling/S2 5_diy2_b=ling/S3 5_diy1_b=ling/S1/002_ling 5_diy2_b=ling/S2/002_ling 5_diy1_b=ling/S3/002_ling 6_diy1_a=ling/S1 6_diy2_a=ling/S2 6_diy1_a=ling/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_single_hard",
"round": 13,
"round": 12,
"seed": 1366160473,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 15,
"total": 36,
"waveId": "act1autochess_h05",
"time": 219.47,
"ticks": 6584,
"reason": "cleared",
"killed": 4,
"total": 72,
"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":4,"total":72,"perfect":false,"leaks":68,"leaksUncounted":0,"coins":0,"dmg":26009,"bossDmg":0,"heal":0,"deaths":28,"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_5_diy1_a",0,null,0,0,102,0,3007,2556,0,15,1],
["p1",2,"op","chess_char_5_diy2_a",1,null,370.98,1,103,0,2160,2185,0,9,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,97,1,2509,2556,0,9,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",584.98,1,104,1,3617,2421,0,10,1],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1002,1,105,0,2111,2004,0,8,2],
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,100,0,2160,3006,0,10,0],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_ling",0,0,94,0,336,3306,0,3,0],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_ling",0,0,63,0,3043,2204,0,6,1],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_ling",0,0,59,1,2987,2204,0,9,0],
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,62,0,568,1704,0,4,0],
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,60,1,2110,1704,0,5,1],
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,82,0,1401,1704,0,5,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]
],
"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,4,14,5015,9427],"enemy_1041_lazerd_2":[24,0,24,22539,9372],"enemy_1042_frostd":[30,0,30,0,7210]},
"hooks": [1,105,6584,234,234,188,188,0,32,100,7,7,0,11027,5,0,72,68,1,32,0,0,0],
"events": {"atk":236,"deploy":31,"die":32,"dmg":188,"leak":68,"skill":14,"spawn":86,"status":55},
"fx": {},
"snaps": "ad5666c6 8d2903a7 535c280f 6d7a9ec9 17cc774e 33b05023 6843f89e be4aaa57 b4314cb1 d5ed8771 93767d45 f24e9eb0 1a8bd02a 979570dd 197f055a 71d6a283 5276a3b4 0a61e8ee fb2aecd2 c2b7f197 1a32846d eea1e52b"
},
"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=ling/S1 6_diy2_b=ling/S2 6_diy1_b=ling/S3 6_diy2_b=ling/S1/002_ling 6_diy1_b=ling/S2/002_ling 6_diy2_b=ling/S3/002_ling 5_diy1_a=radian/S1 5_diy2_a=radian/S2 5_diy1_a=radian/S3 5_diy2_b=radian/S1 5_diy1_b=radian/S2 5_diy2_b=radian/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m03",
@@ -4624,38 +4624,289 @@
"round": 13,
"seed": 500258608,
"waveId": "act1autochess_h06",
"time": 215.33,
"ticks": 6460,
"time": 216.33,
"ticks": 6490,
"reason": "cleared",
"killed": 15,
"killed": 0,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 453,
"rngDraws": 0,
"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":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":31059,"bossDmg":0,"heal":0,"deaths":30,"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",0,0,143,0,2468,3006,0,8,1],
["p1",2,"op","chess_char_6_diy2_b",1,"none",0,0,150,0,2619,3006,0,7,1],
["p1",3,"op","chess_char_6_diy1_b",2,"none",0,0,137,0,1800,3006,0,6,0],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_ling",0,0,134,0,1400,3456,0,4,0],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_ling",0,0,155,0,6030,3456,0,11,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_ling",0,0,156,0,2400,3456,0,8,0],
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,91,0,480,1688,0,2,1],
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,104,0,2560,1688,0,10,0],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,86,0,5570,1688,0,15,1],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,80,0,1776,1986,0,6,0],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,77,0,1687,1986,0,5,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,122,0,2270,1986,0,7,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_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_1019_jshoot_2":[57,0,57,27780,7110],"enemy_1299_ymkilr_2":[27,0,27,993,355],"enemy_1422_lrsldr_2":[39,0,39,1635,23594]},
"hooks": [1,156,6490,197,197,187,187,0,30,153,7,7,0,9863,16,7,123,123,1,30,0,0,0],
"events": {"atk":198,"deploy":30,"die":30,"dmg":187,"fx":1,"leak":123,"skill":14,"spawn":138,"status":20},
"fx": {"shield":1},
"snaps": "d7f36ed1 f5e46418 3a551e45 e61333e2 0aebab62 7ced810f 9eaa069c 813d371f 9388e5a8 41eaf294 199ba4e0 b8912949 1ae3ff86 5c52c7de 77a31a46 f807371e 91c5b146 2cd0f1c2 0e04027e 2218d08c 30595146 c3e230a4"
},
"diy-110": {
"about": "5_diy1_b=radian/S1/002_radian 5_diy2_b=radian/S2/002_radian 5_diy1_b=radian/S3/002_radian 5_diy2_b=radian/S1/003_radian 5_diy1_b=radian/S2/003_radian 5_diy2_b=radian/S3/003_radian 6_diy1_a=radian/S1 6_diy2_a=radian/S2 6_diy1_a=radian/S3 6_diy2_b=radian/S1 6_diy1_b=radian/S2 6_diy2_b=radian/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_multi_hard",
"round": 13,
"seed": 2140781790,
"waveId": "act1autochess_h06",
"time": 224.23,
"ticks": 6727,
"reason": "cleared",
"killed": 0,
"total": 144,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":144,"perfect":false,"leaks":144,"leaksUncounted":0,"coins":0,"dmg":29877,"bossDmg":0,"heal":0,"deaths":31,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_radian",0,0,170,0,4597,3354,0,18,1],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_radian",0,0,175,0,4272,3354,0,13,1],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_radian",0,0,154,0,2072,3354,0,9,0],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_radian",0,0,162,0,1853,2979,0,8,0],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_radian",0,0,176,0,4006,2979,0,13,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_radian",0,0,177,0,2813,2979,0,11,0],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,88,0,1200,1688,0,6,0],
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,150,0,1264,2532,0,7,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,97,0,2472,1688,0,12,0],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,96,0,1554,1986,0,7,0],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,91,0,1776,1986,0,8,0],
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,86,0,1998,1986,0,9,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]
],
"enemies": {"enemy_1251_lysyta_2":[51,0,51,2126,23168],"enemy_1252_lysytb_2":[54,0,54,28739,22654],"enemy_1329_cbshld_2":[39,0,39,0,17855]},
"hooks": [1,177,6727,213,213,1030,1030,0,31,175,3,3,0,6821,0,4,144,144,1,31,0,0,0],
"events": {"atk":213,"deploy":31,"die":31,"dmg":1029,"fx":1,"leak":144,"skill":6,"spawn":158},
"fx": {"shield":1},
"snaps": "baec0fa1 ae34a8bc bbe170e7 1f4f4123 be3280e2 074a7782 ef2848a2 ab582a7a f83905a6 171327f0 ed230f94 8ccd0af8 f562479c 0759e60b 929cf606 e15057cc f0fd5820 72120892 fd043ea2 db135d0d 5083a139 3e3f64be a32fda3d"
},
"diy-111": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=radian/S1/002_radian 6_diy2_b=radian/S2/002_radian 6_diy1_b=radian/S3/002_radian 6_diy2_b=radian/S1/003_radian 6_diy1_b=radian/S2/003_radian 6_diy2_b=radian/S3/003_radian 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_single_normal",
"round": 13,
"seed": 452407399,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 16,
"total": 150,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":16,"total":150,"perfect":false,"leaks":134,"leaksUncounted":0,"coins":0,"dmg":381807,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,777.64,1,176,1,7991,5606,0,53,1],
["p1",2,"op","chess_char_5_diy2_b",2,"none",454.03,1,172,3,7218,5362,0,119,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,165,1,11019,7692,0,31,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,166,1,14682,10002,0,39,1],
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_radian",1158,1,4,3,45074,0,0,135,4],
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_radian",1158,1,5,0,56059,0,0,127,4],
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_radian",1158,1,11,2,75403,0,0,137,3],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_radian",993,1,13,1,39737,0,0,129,4],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_radian",993,1,6,1,55606,0,0,114,4],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_radian",993,1,14,1,54183,0,0,130,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,177,0,809,3813,0,14,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,171,2,14026,4530,0,39,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_1249_lysdb_2":[60,7,52,14551,210855],"enemy_1251_lysyta":[51,9,42,5311,96245],"enemy_1329_cbshld":[39,0,38,17144,74707]},
"hooks": [1,177,6900,1402,1402,1401,1345,0,31,163,28,27,0,33443,222,37,150,132,1,31,0,0,0],
"events": {"atk":1402,"deploy":24,"die":31,"dmg":1345,"fx":64,"leak":132,"skill":55,"spawn":165,"status":258},
"fx": {"shield":8,"shieldBreak":56},
"snaps": "a5c6c412 50fc8ffb cac1416a 96a2d2fd 22a41418 91ea640c 8ec75145 e7c943a7 65ec8e63 b388e3cf 1c0acedf 0df20a06 4c892963 c6544fb2 6540743f 6c533113 f2e64eee c037effa b31cba13 df972f1b 6cc232f7 81b685ae f3768778"
},
"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=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 3106580722,
"waveId": "act1autochess_h05",
"time": 197.6,
"ticks": 5928,
"reason": "cleared",
"killed": 5,
"total": 135,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 62,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":5,"total":135,"perfect":false,"leaks":130,"leaksUncounted":0,"coins":0,"dmg":185275,"bossDmg":0,"heal":1681,"deaths":24,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,3,0,2926,2604,0,22,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,1,1,20026,3371,0,42,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1529,1,160,1,23977,3058,0,14,2],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,100,1,55389,3808,0,31,3],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,125,0,17384,5276,0,16,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,149,1,33388,6822,0,19,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,104,0,6865,2266,0,17,1],
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,98,0,16470,3087,0,37,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,159,0,0,4208,1476,3,1],
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,95,0,0,3508,206,1,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,137,0,1304,2796,0,4,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,109,1,7547,2262,0,19,1]
],
"enemies": {"enemy_2021_syfish":[105,5,100,10120,180575],"enemy_2025_syufo":[30,0,30,32947,4699]},
"hooks": [1,160,5928,492,492,643,752,4,29,159,13,13,0,12002,22,25,135,130,1,50,0,13,16],
"events": {"atk":492,"deploy":25,"die":29,"dmg":752,"fx":103,"heal":4,"leak":130,"skill":26,"spawn":147,"status":96},
"fx": {"aoe":30,"burst":13,"dodge":16,"dp":7,"slash":37},
"snaps": "3c77b2f2 2d68717f e1b87890 e08ce3ac 8a5e7f1f 31ef2cc5 8b253efe 370529ae 3775dd9d 50244d35 5a945311 165da72c 11bc641a ffc8aafe 9cfca40e dd5a9212 99088f8a b69dee9f 2eea76c2 190540f3"
},
"diy-113": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_multi_funny",
"round": 12,
"seed": 1466195980,
"waveId": "act1autochess_h05",
"time": 211.27,
"ticks": 6338,
"reason": "cleared",
"killed": 151,
"total": 189,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 770,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":151,"total":189,"perfect":false,"leaks":38,"leaksUncounted":0,"coins":0,"dmg":422472,"bossDmg":0,"heal":50683,"deaths":3,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,1865,1,204,5,28825,1865,0,96,2],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,200,63,91421,3702,0,121,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,179,1,15866,19963,0,168,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,11,4,39522,20556,0,195,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,8,17,17523,0,0,96,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,12,15,48134,0,0,116,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,14,12,30073,0,0,50,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,4,26,80564,0,0,71,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,6,0,0,0,26547,60,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,9,0,0,9990,24136,55,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,13,6,23978,0,0,139,2],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,15,2,46566,0,0,129,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": [18,16,2,121,39812],
"enemy_1197_sfshu_2": [27,22,5,11140,77179],
"enemy_1198_msfshu": [48,40,8,0,123],
"enemy_1198_msfshu_2": [66,56,10,0,234],
"enemy_9010_acpupp": [30,17,13,44816,305124]
},
"hooks": [1,207,6338,1448,1448,1915,1565,173,154,192,20,21,0,36912,6057,35,189,38,1,177,0,0,0],
"events": {"atk":1625,"deploy":15,"die":154,"dmg":1563,"fx":187,"heal":142,"leak":38,"skill":46,"spawn":204,"status":841},
"fx": {"aoe":10,"dodge":10,"ember":23,"revive":2,"shieldBreak":78,"spGain":3,"summon":38,"telegraph":23},
"snaps": "ef5e10e1 f8ef6202 0856fbf3 743f7e3c bdd7c9df fbb1f33e d7aef25c 62f3b029 6462dcba a814041c 5c101efb e2611c0f 4077ab11 206c4c48 2e64350a 0cb2085c 5cf33d8a 34d88b74 6d33ce13 1a4e3d75 902d09d3 34dd3e78"
},
"diy-114": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m04",
"modeId": "mode_single_hard",
"round": 13,
"seed": 4120508441,
"waveId": "act1autochess_h06",
"time": 211.47,
"ticks": 6344,
"reason": "cleared",
"killed": 108,
"total": 183,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 552,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":108,"total":183,"perfect":false,"leaks":75,"leaksUncounted":0,"coins":0,"dmg":569355,"bossDmg":0,"heal":49617,"deaths":16,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,170,2,646,5208,0,11,1],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",3431,1,207,3,16580,3431,0,28,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,178,6,1769,3058,0,7,2],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,152,13,48051,7165,0,39,3],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,194,3,45541,20360,0,67,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",1,1,205,3,73708,14091,0,70,3],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,4,4,15133,0,0,34,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,5,39,102309,0,0,185,3],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,15,2,190213,0,0,75,2],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,157,11,75404,3638,0,29,1],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,145,0,0,5382,14474,25,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,141,0,0,24140,35143,45,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,10,5,3608,50780],
"enemy_1197_sfshu_2": [39,20,19,19927,153485],
"enemy_1198_msfshu": [30,21,9,0,67],
"enemy_1198_msfshu_2": [60,47,13,0,195],
"enemy_9010_acpupp": [39,10,29,49219,443858]
},
"hooks": [1,207,6344,800,800,3158,2921,145,124,199,25,23,0,25007,0,55,183,75,1,144,0,0,15],
"events": {"atk":991,"deploy":21,"die":124,"dmg":2921,"fx":210,"heal":125,"leak":75,"skill":48,"spawn":198,"status":22},
"fx": {"aoe":45,"dodge":15,"dp":24,"ember":11,"revive":1,"shieldBreak":35,"slash":36,"spGain":2,"summon":30,"telegraph":11},
"snaps": "acd854e9 b9f7cc4f 3712bbfa 1c0f22b1 f383429e eef02120 58be1170 cac5fb4b fb5760b0 40f677c1 61597d23 3c76af95 16bbb8e1 63da3fda 1474707d 354bc689 ac3b2399 2d6718dd 4577d3fd cc919f09 6ccbba2f 92e55460"
},
"diy-115": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m05",
"modeId": "mode_multi_normal",
"round": 13,
"seed": 2479968306,
"waveId": "act1autochess_h06",
"time": 225.33,
"ticks": 6760,
"reason": "cleared",
"killed": 12,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 169,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":12,"total":123,"perfect":false,"leaks":111,"leaksUncounted":0,"coins":0,"dmg":249484,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,146,4,32431,5595,0,37,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,144,5,64091,7482,0,44,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,147,0,14524,8078,0,83,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,145,2,43730,10388,0,85,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,5,0,31540,0,0,62,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,6,0,63168,0,0,91,2]
],
"enemies": {"enemy_1006_shield_3":[21,0,21,8087,71414],"enemy_1043_zomsbr":[66,12,54,1190,188671],"enemy_1402_tgshd_2":[36,0,36,22266,105615]},
"hooks": [1,147,6760,462,462,3535,3598,30697,32,143,6,20,0,11458,6246,32,123,111,1,32,0,0,0],
"events": {"atk":509,"deploy":24,"die":32,"dmg":3505,"fx":340,"leak":111,"skill":44,"spawn":129,"status":789},
"fx": {"displace":19,"expose":321},
"snaps": "ddc538a1 cbceb698 326b4898 f6e27d08 15a0f45e af16dd25 93dea0ab 97d57fc4 a8846bcd 6e0d1701 06f10639 94321c5b bd3f8f0a a16c3881 f2bfdae7 ad792514 96873558 72ca0dbd 940adf20 150cbbe6 06e3dc3b 58c0231f 29f31a26"
}
}
}