feedback5 O24: 自选 kits for 凯尔希·思衡托 (with her 战术锚点) and Mon3tr (with her 重构体)

Two owned 6★ picks get their operator kits (kits/ops/op-kalts2.js, op-monstr.js; OPERATOR_KIT_FILES):
- 凯尔希·思衡托 (char_1052_kalts2, 守望者, no module): heals that pass 凯尔希's Mon3tr; 遗尘守望 (HP / DEF +25 %,
  block +1, 阻挡半径倍率 +0.23, 起飞); 医者丰碑 (operators entering her range: a 1-hit 护盾 and 生命回复速度 +50 / 罗德岛
  +100 for 30 s); S1 (ATK / ASPD, other 起飞 operators' 阻挡半径倍率); S2 (y-11, 10 shots: true damage + 停顿 and a
  heal within 1.5, at an enemy or an injured ally); S3 (ATK, interval −1.55, 2 targets; with her placed anchor: two
  operators withdrawn into 【待投放】 and back, her flight and 【移动】 onto the anchor). The anchor's kit (无敌, never
  attacked); content/tokens.js docks it as a skill's summon.
- Mon3tr (char_4179_monstr, 链愈师, XAH-X): her own chain (x-4 jumps, ×0.75 / ×0.85, free links), the 重构体's kit
  (禁疗 but hers, 流失, ATK aura, its return), 战术协同 ASPD, S1 next heal, S2 (the 重构体 first, a second chain
  0.6 s on), S3 (needs the 重构体: the move onto its tile, true melee, 不死, the 【返回】).
Engine (additive): mod `blockRadiusScale` = 阻挡半径倍率 − 1 (buffs.js, units.js s.blockRadiusScale): the air-block
radius of Battle._checkBlock is 0.8944 × (1 + it) — 0 for every other unit. docs/SIM.md §1.2 / §2 / §3, DESIGN §5.5.
Tests: test/content/op_kalts2.test.js (10), test/content/op_monstr.test.js (10).
Golden: only diy moved — diy-110 … diy-112 are new and diy-106 … diy-109 re-packed (the two kits' configurations
join the family's 12-per-battle packing); roster, bonds, fields, matches and standins are byte-identical.
This commit is contained in:
sganggs committed 2026-10-06 05:10:33 +08:00
1 parent 996f5d20fe
commit 0451860cf9
11 files changed
+1667 -105

No files matched your search

+360
View File
@@ -0,0 +1,360 @@
// test/content/op_kalts2.test.js — the 自选 operator kit of 凯尔希·思衡托 (char_1052_kalts2, 6★ 守望者; kit
// server/sim/content/kits/ops/op-kalts2.js) and her summon 战术锚点 (token_10068_kalts2_mtship), fielded the production way (a
// DIY slot + its `diy` pick; the anchor as a token piece of hers) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4)
// and elite (E2 Lv60, rank 7) — she has no module. Every number is read back from data/backups.json (the form of that slot
// status, the token's variant); the fidelity checklist of kits/README.md item by item.
// Run: node --test test/content/op_kalts2.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';
import { COLS } from '../../server/sim/constants.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 KALTS2 = 'char_1052_kalts2';
const ANCHOR = 'token_10068_kalts2_mtship';
const FORMS = BACKUPS.units[KALTS2].forms;
const TOKEN = BACKUPS.tokens[ANCHOR];
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const S1 = 'skchr_kalts2_1', S2 = 'skchr_kalts2_2', S3 = 'skchr_kalts2_3';
const statusKey = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusKey(tier, elite)];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const talentOf = (tier, elite, i) => formOf(tier, elite).talents.find((t) => t.index === i).bb;
const variantOf = (tier, elite) => TOKEN.variants[`${KALTS2}@${statusKey(tier, elite)}`];
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_biter: dummy('enemy_biter', { atk: 400, range: 1.2, bat: 1 }),
enemy_flybiter: dummy('enemy_flybiter', { atk: 300, range: 1.6, bat: 1, motion: 'FLY' }),
};
/** Every 自选 form: [tier, elite] (no module). */
const FORMS_ALL = [[5, false], [6, false], [5, true], [6, true]];
const label = ([tier, elite]) => `T${tier} ${elite ? 'elite' : 'normal'}`;
const YAK = 'chess_char_1_02_a', TEXAS = 'chess_char_1_08_a', PERFUMER = 'chess_char_2_14_a';
/** A battle with 凯尔希·思衡托 as uid 1 at (10, 4) facing RIGHT (`anchor`: her piece as uid 2), plus `others`. */
function field({ tier = 5, elite = false, skill = 0, anchor = null, others = [], seed = 5, dp = 99 } = {}) {
const units = [{ uid: 1, diy: { slot: SLOT[tier], charId: KALTS2, skillIndex: skill }, elite, row: 10, col: 4 }];
if (anchor) units.push({ uid: 2, kind: 'token', tokenId: ANCHOR, ownerUid: 1, row: anchor.row, col: anchor.col, dir: anchor.dir ?? 'RIGHT' });
const log = []; // heal / damaged in the order they happen
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpInit: dp, dpPerSec: 0, dpMax: 999 },
hooks: ['damaged', 'heal', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'blocked'], captureNoisy: true,
units: [...units, ...others],
setup: (b) => {
b.on('heal', (c) => { if (!c.opts?.regen) log.push({ name: 'heal', ...c }); }, { priority: -1000 });
b.on('damaged', (c) => log.push({ name: 'damaged', ...c }), { priority: -1000 });
},
});
h.step();
return { h, u: h.unit(1), a: anchor ? h.unit(2) : null, log };
}
const heals = (h, u) => h.hooksOf('heal').filter((c) => c.source === u && !c.opts?.regen);
const inRange = (u, x) => u.rangeKeySet.has(x.tileR * COLS + x.tileC);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('凯尔希·思衡托 in every 自选 form: her operator kit (all three skills authored), the form\'s stats with 遗尘守望, y-6, one heal per attack, blocks 2, 起飞, 罗德岛, no module, no 特质; her 战术锚点: the form\'s token stats', () => {
assert.equal(OPERATOR_KITS[KALTS2], KITS[KALTS2]);
for (const f of FORMS_ALL) {
const [tier, elite] = f;
for (const skill of [0, 1, 2]) {
const { h, u, a } = field({ tier, elite, skill, anchor: { row: 10, col: 9 } });
const form = formOf(tier, elite), t0 = talentOf(tier, elite, 0);
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [KALTS2, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.bat, u.base.cost], [form.stats.maxHp, form.stats.atk, form.stats.def, 2.85, 19], `${label(f)}: stats`);
approx(u.s.maxHp, form.stats.maxHp * (1 + t0.max_hp), `${label(f)}: 生命上限 +25 %`);
approx(u.s.def, form.stats.def * (1 + t0.def), `${label(f)}: 防御力 +25 %`);
assert.deepEqual([u.s.blockCnt, u.s.blockRadiusScale, t0.block_cnt, t0.block_radius_scale], [2, 0.23, 1, 0.23], `${label(f)}: 阻挡数 +1, 阻挡半径倍率 +0.23`);
assert.deepEqual([u.profile.dmgType, u.profile.heal, u.profile.attack, u.def.subProf], ['heal', { mode: 'single' }, 'ranged', 'watchman'], `${label(f)}: 守望者 heal`);
assert.deepEqual([u.s.flags.liftoff, u.s.flags.blockFly], [true, true], `${label(f)}: 起飞`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-6`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.tokens, u.def.raw.nationId, !!u.def.raw.module?.active, form.modules?.length ?? 0],
[BACKUPS.diy.operators[KALTS2].bonds, [], [ANCHOR], 'rhodes', false, 0], `${label(f)}: bonds / 特质 / summon / no module`);
const ts = variantOf(tier, elite).stats;
assert.deepEqual([a.base.maxHp, a.base.atk, a.base.cost, a.base.respawnTime, a.s.blockCnt], [ts.maxHp, 0, 3, 70, 0], `${label(f)}: 战术锚点 stats`);
assert.deepEqual([a.s.flags.invulnerable, a.s.flags.untargetable, a.profile.noAttack, !!a.kit.generic], [true, true, true, false], `${label(f)}: 战术锚点 无敌, never targeted, no attack`);
assert.equal(a.alive, skill === 2, `${label(f)}: the anchor piece takes the field only with S3 (a skill's summon)`);
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1393 / 408 / 220, E2 Lv60 1543 / 476 / 257; the anchor 2235 HP, 3 DP, 70 s
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/3'].stats.atk], [1393, 408, 1543, 476]);
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(6, true).stats.cost, TOKEN.placeable], [2235, 3, true]);
});
test('a 自选 pick: 凯尔希·思衡托 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(KALTS2));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(KALTS2), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: KALTS2, skillIndex: 2 } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: KALTS2, skillIndex: 2, uniEquipId: null } } });
});
test('trait: her heals take 凯尔希\'s Mon3tr through its 禁疗 (her selection too); another 禁疗 summon stays out', () => {
const others = [{ uid: 3, diy: { slot: 6, charId: 'char_003_kalts', skillIndex: 0 }, row: 12, col: 2 },
{ uid: 4, kind: 'token', tokenId: 'token_10002_kalts_mon3tr', ownerUid: 3, row: 10, col: 5 }];
const { h, u } = field({ skill: 0, others });
const m = h.unit(4);
assert.ok(m.s.flags.noHeal && inRange(u, m), '凯尔希\'s Mon3tr: 禁疗, in her range');
m.hp = m.s.maxHp * 0.4;
const n0 = heals(h, u).length;
assert.ok(h.runUntil(() => heals(h, u).length > n0, 4), 'a heal');
assert.equal(heals(h, u)[n0].target, m, 'it is her target');
approx(heals(h, u)[n0].amount, u.s.atk, 'a full heal through its 禁疗');
done(h);
});
test('T1 遗尘守望 起飞: no ground enemy targets her or is blocked by her; she blocks a flyer 1.0 tile away (air radius 0.8944 × 1.23), two at most; the mod blockRadiusScale on any blocker', () => {
const { h, u } = field({ skill: 0, others: [{ uid: 3, chessId: YAK, row: 11, col: 7 }] });
const yak = h.unit(3);
const g = h.spawn('enemy_biter', { pos: [10, 5] });
const f1 = h.spawn('enemy_fly', { pos: [10, 5] });
const f2 = h.spawn('enemy_fly', { pos: [11, 4] });
const f3 = h.spawn('enemy_fly', { pos: [9, 4] });
h.run(3);
assert.equal(g.blockedBy, null, 'a ground enemy on the next tile is not blocked');
assert.equal(h.hooksOf('damaged').filter((c) => c.target === u).length, 0, 'nor does it attack her (对地规避)');
assert.equal([f1, f2, f3].filter((e) => e.blockedBy === u).length, 2, 'two of the three flyers 1.0 away: block 2');
assert.ok(f1.blockedBy === u, 'the first one in contact');
// the engine rule on another unit: a blockFly unit blocks a flyer 1.0 away only with 阻挡半径倍率 ≥ 1.118
const f4 = h.spawn('enemy_fly', { pos: [11, 8] });
h.b.addBuff(yak, { key: 'test:air', flags: { blockFly: true } });
h.run(0.5);
assert.equal(f4.blockedBy, null, 'radius 0.8944: out of reach');
h.b.addBuff(yak, { key: 'test:scale', mods: { blockRadiusScale: 0.23 } });
h.run(0.5);
assert.equal(f4.blockedBy, yak, 'radius 0.8944 × 1.23: blocked');
assert.equal(yak.s.blockRadiusScale, 0.23);
done(h);
});
test('T2 医者丰碑: an operator entering her range gets 1 护盾 layer (one damage instance blocked) and 生命回复速度 +50 for 30 s (罗德岛 +100); summons and she get nothing; a new entry refreshes, never a 2nd layer', () => {
for (const f of [[5, false], [6, true]]) {
const [tier, elite] = f;
const t1 = talentOf(tier, elite, 1);
assert.deepEqual([t1.buff_duration, t1.hp_recovery_per_sec, t1.rhodes_bonus], [30, 50, 2], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }, { uid: 4, chessId: PERFUMER, row: 9, col: 5 },
{ uid: 5, chessId: TEXAS, row: 12, col: 8 }, { uid: 6, diy: { slot: 6, charId: 'char_003_kalts', skillIndex: 0 }, row: 12, col: 2 },
{ uid: 7, kind: 'token', tokenId: 'token_10002_kalts_mon3tr', ownerUid: 6, row: 11, col: 5 }];
const { h, u } = field({ tier, elite, skill: 0, others });
const yak = h.unit(3), pf = h.unit(4), tx = h.unit(5), m = h.unit(7);
assert.equal(pf.def.raw.nationId, 'rhodes');
assert.ok(yak.findBuff('talent:kalts2:shield') && yak.findBuff('talent:kalts2:shield').shieldHits === 1, `${label(f)}: 角峰 1 护盾 layer`);
assert.deepEqual([yak.findBuff('talent:kalts2:regen')?.mods, pf.findBuff('talent:kalts2:regen')?.mods], [{ hpRegen: 50 }, { hpRegen: 100 }], `${label(f)}: 50 / 罗德岛 100 HP/s (生命回复速度)`);
approx(yak.findBuff('talent:kalts2:regen').timeLeft, 30, `${label(f)}: 30 s`, 0.01);
assert.ok(!m.findBuff('talent:kalts2:shield') && !m.findBuff('talent:kalts2:regen'), `${label(f)}: no summon`);
assert.ok(!u.findBuff('talent:kalts2:shield') && !u.findBuff('talent:kalts2:regen'), `${label(f)}: not herself`);
assert.ok(!tx.findBuff('talent:kalts2:regen'), `${label(f)}: 德克萨斯 outside her range`);
// the 护盾 blocks one damage instance whole
const hp0 = yak.hp;
assert.equal(h.b.dealDamage(null, yak, { amount: 500, type: 'true', sourceless: true }), 0, `${label(f)}: blocked`);
assert.equal(yak.hp, hp0);
assert.equal(yak.findBuff('talent:kalts2:shield'), null, `${label(f)}: used up`);
assert.ok(h.b.dealDamage(null, yak, { amount: 500, type: 'true', sourceless: true }) > 0, `${label(f)}: the next one lands`);
// still inside: no new entry; out and back in: a new layer and a fresh 30 s
h.run(5);
assert.equal(yak.findBuff('talent:kalts2:shield'), null, `${label(f)}: no entry while it stays`);
approx(yak.findBuff('talent:kalts2:regen').timeLeft, 25, `${label(f)}: ticking`, 0.05);
h.b.relocate(yak, 10, 8);
h.step();
assert.ok(!inRange(u, yak));
h.b.relocate(yak, 10, 5);
h.step();
assert.ok(yak.findBuff('talent:kalts2:shield'), `${label(f)}: re-entry: 护盾 again`);
approx(yak.findBuff('talent:kalts2:regen').timeLeft, 30, `${label(f)}: re-entry: 30 s again`, 0.05);
h.b.relocate(yak, 10, 8); h.step(); h.b.relocate(yak, 10, 5); h.step();
assert.equal(yak.buffs.filter((b) => b.key === 'talent:kalts2:shield').length, 1, `${label(f)}: one layer at most`);
// 30 s on: the regen ends (德克萨斯 entering later gets its own)
const r0 = tx.s.hpRegen; // (调香师's own 熏衣草 regen on everyone)
assert.ok(h.b.relocate(tx, 9, 3));
h.step();
approx(tx.s.hpRegen - r0, 50, `${label(f)}: 德克萨斯 enters`);
assert.deepEqual(tx.findBuff('talent:kalts2:regen')?.mods, { hpRegen: 50 });
done(h);
}
});
test('S1 应急肃正防线 (MANUAL, data DEFAULT — as she is about to heal): 35 s, ATK +60 % / +75 %, ASPD +25 / +35; every other 起飞 operator 阻挡半径倍率 +0.23 meanwhile (not a grounded one, not her twice)', () => {
for (const f of [[5, false], [6, true]]) {
const [tier, elite] = f;
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.attack_speed, sk.bb['attack@block_radius_scale'], sk.spCost, sk.initSp, sk.trigger.rule],
[35, elite ? 0.75 : 0.6, elite ? 35 : 25, 0.23, 35, elite ? 20 : 18, 'DEFAULT'], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }, { uid: 4, chessId: TEXAS, row: 12, col: 9 }];
const { h, u } = field({ tier, elite, skill: 0, others });
const yak = h.unit(3), tx = h.unit(4);
h.b.addBuff(tx, { key: 'test:liftoff', flags: { liftoff: true } });
assert.equal(u.skill.rule, 'DEFAULT');
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, `${label(f)}: nobody injured: no cast`);
yak.hp = yak.s.maxHp * 0.5;
assert.ok(h.runUntil(() => u.skill.active, 4), `${label(f)}: cast as she heals`);
approx(u.skill.timeLeft, 35, `${label(f)}: 35 s`, 0.05);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
assert.equal(u.s.aspd, u.base.aspd + sk.bb.attack_speed, `${label(f)}: ASPD`);
h.run(0.4);
assert.equal(tx.s.blockRadiusScale, 0.23, `${label(f)}: the 起飞 德克萨斯 (anywhere on the field)`);
assert.equal(yak.s.blockRadiusScale, 0, `${label(f)}: 角峰 is not 起飞`);
assert.equal(u.s.blockRadiusScale, 0.23, `${label(f)}: hers stays 0.23 (同名效果取最高)`);
h.runUntil(() => !u.skill.active, 40);
h.run(0.5);
assert.deepEqual([tx.s.blockRadiusScale, u.s.atk, u.s.aspd], [0, u.base.atk, u.base.aspd], `${label(f)}: all back`);
done(h);
}
});
test('S2 保护性拒止 (MANUAL, data ACTIVE_RANGE on its y-11: an injured ally there): 10 shots, range y-11, ATK +; at an enemy (air too): true damage + 5 s 停顿 within 1.5, then the heal there; at an ally (no enemy): heal first, no 停顿', () => {
for (const f of [[5, false], [6, true]]) {
const [tier, elite] = f;
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.bb.atk, sk.bb['attack@atk_scale'], sk.bb['attack@heal_scale'], sk.bb['attack@sluggish'], sk.bb['attack@trigger_time'], sk.rangeId, sk.trigger.rule],
[elite ? 1.25 : 1.1, elite ? 3.5 : 3.1, elite ? 1.7 : 1.35, 5, 10, 'y-11', 'ACTIVE_RANGE'], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 7 }, { uid: 4, chessId: TEXAS, row: 11, col: 4 }];
const { h, u, log } = field({ tier, elite, skill: 1, others });
const yak = h.unit(3);
const mine = (from) => log.slice(from).filter((c) => c.source === u);
assert.deepEqual([u.skill.rule, u.skill.kind], ['ACTIVE_RANGE', 'ammo'], label(f));
assert.ok(!inRange(u, yak), `${label(f)}: (10,7) is outside her y-6`);
u.skill.gainSp(999);
h.run(1.5);
assert.equal(u.skill.activations, 0, `${label(f)}: nobody injured: no cast`);
// an ally shot first: 角峰 injured inside the y-11 only, no enemy she can select (the dummy is beyond it)
const e = h.spawn('enemy_dummy', { pos: [11, 8] }); // √2 from 角峰, outside the y-11 (col +4)
const n0 = log.length;
yak.hp = yak.s.maxHp * 0.3;
assert.ok(h.runUntil(() => u.skill.active, 0.5), `${label(f)}: cast for the y-11`);
assert.deepEqual([u.liveRangeGrid, u.skill.ammoMax], [sk.rangeGrid, 10], `${label(f)}: y-11, 10 shots`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
assert.ok(h.runUntil(() => u.skill.ammoLeft === 9, 3), `${label(f)}: one shot (the cast's attack)`);
const shot = mine(n0);
assert.deepEqual(shot.slice(0, 2).map((c) => [c.name, c.target]), [['heal', yak], ['damaged', e]], `${label(f)}: at an ally — heal first, then the enemy within 1.5`);
approx(shot[0].amount, u.s.atk * sk.bb['attack@heal_scale'], `${label(f)}: heal ${sk.bb['attack@heal_scale'] * 100} % ATK`);
approx(shot[1].amount, u.s.atk * sk.bb['attack@atk_scale'], `${label(f)}: ${sk.bb['attack@atk_scale'] * 100} % ATK true`);
assert.equal(h.hooksOf('statusApplied').filter((c) => c.source === u).length, 0, `${label(f)}: no 停顿 from an ally shot`);
// an enemy inside the y-11 is the target from now on — a flyer
const fly = h.spawn('enemy_fly', { pos: [9, 7] }); // 1.0 from 角峰
const n1 = log.length;
assert.ok(h.runUntil(() => mine(n1).some((c) => c.name === 'damaged'), 4), `${label(f)}: another shot lands (a 医疗单元 in flight)`);
const burst = mine(n1);
assert.equal(burst[0].name, 'damaged', `${label(f)}: at an enemy — damage first`);
assert.equal(burst[0].target, fly, `${label(f)}: the flyer (可对空)`);
for (const c of burst.filter((x) => x.name === 'damaged')) assert.deepEqual([c.type, c.dmg.isAttack, c.dmg.isSplash, c.dmg.isSkill], ['true', true, false, true], `${label(f)}: 真实普通伤害`);
assert.ok(burst.some((c) => c.name === 'heal' && c.target === yak), `${label(f)}: then 角峰 (1.0 away) healed`);
assert.ok(!burst.some((c) => c.target === e), `${label(f)}: the dummy (2.2 from the flyer) untouched`);
const slugs = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'sluggish');
assert.ok(slugs.length >= 1 && slugs.every((c) => c.duration === 5 && c.target === fly), `${label(f)}: 停顿 5 s on the enemy struck`);
h.runUntil(() => !u.skill.active, 40);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `${label(f)}: back to y-6 after 10 shots`);
done(h);
}
});
test('S3 破梏重生 (MANUAL, data DEFAULT): 35 s, ATK +, attack interval 1.30 s (−1.55 flat), 2 heal targets; with no anchor she stays', () => {
for (const f of [[5, false], [6, true]]) {
const [tier, elite] = f;
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.spCost, sk.initSp, sk.trigger.rule, sk.overrideTokenKey],
[35, elite ? 1.25 : 1.1, -1.55, elite ? 60 : 65, elite ? 30 : 25, 'DEFAULT', ANCHOR], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }, { uid: 4, chessId: TEXAS, row: 11, col: 5 }];
const { h, u } = field({ tier, elite, skill: 2, others });
const yak = h.unit(3), tx = h.unit(4);
u.skill.gainSp(999);
yak.hp = yak.s.maxHp * 0.3; tx.hp = tx.s.maxHp * 0.4;
const n0 = heals(h, u).length;
assert.ok(h.runUntil(() => u.skill.active, 4), label(f));
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
approx(u.s.interval, 2.85 - 1.55, `${label(f)}: 1.30 s`);
h.runUntil(() => heals(h, u).length >= n0 + 2, 3);
const two = heals(h, u).slice(n0, n0 + 2); // the cast's heal (same tick) already heals two
assert.deepEqual(two.map((c) => c.target.id).sort(), [yak.id, tx.id].sort(), `${label(f)}: two targets per heal`);
assert.equal(two[0].t, two[1].t, `${label(f)}: in one attack`);
h.run(2);
assert.deepEqual([u.tileR, u.tileC, u.hidden], [10, 4, false], `${label(f)}: no anchor: no move`);
h.runUntil(() => !u.skill.active, 40);
approx(u.s.interval, 2.85, `${label(f)}: interval back`);
done(h);
}
});
test('S3 with her anchor: 0.1 s after the cast she flies to it (hidden, 无敌, the skill time paused, 3.5 tiles/s; operators within 2.2 of her flight enter T2), the two latest-deployed operators of her range are withdrawn 0.6 s on and back at once; she lands on its tile facing its way; the anchor is withdrawn for the rest of the skill', () => {
for (const f of [[5, false], [6, true]]) {
const [tier, elite] = f;
// deployment order (left column first, top row first): (11,5) → (10,5) → (9,5); (12,7) beside her flight
const others = [{ uid: 3, chessId: YAK, row: 11, col: 5 }, { uid: 4, chessId: TEXAS, row: 10, col: 5 }, { uid: 5, chessId: PERFUMER, row: 9, col: 5 },
{ uid: 6, chessId: 'chess_char_2_02_a', row: 12, col: 7 }];
const { h, u, a } = field({ tier, elite, skill: 2, anchor: { row: 10, col: 9, dir: 'UP' }, others });
const yak = h.unit(3), tx = h.unit(4), pf = h.unit(5), sil = h.unit(6);
assert.ok(a.alive && u.mem.k2link === a, `${label(f)}: the anchor deployed at the start, linked to her`);
assert.ok(!inRange(u, a), `${label(f)}: outside her range`);
assert.ok(!sil.findBuff('talent:kalts2:regen'), `${label(f)}: 赫默 (12,7) is outside her range`);
const seq = [yak.deploySeq, tx.deploySeq, pf.deploySeq];
assert.ok(seq[0] < seq[1] && seq[1] < seq[2], label(f));
u.skill.gainSp(999);
yak.hp = yak.s.maxHp * 0.3;
assert.ok(h.runUntil(() => u.skill.active, 4), label(f));
const t0 = h.b.time;
h.runUntil(() => u.hidden, 0.2);
assert.ok(u.hidden && u.mem.k2flight, `${label(f)}: flying`);
approx(h.b.time - t0, 0.1, `${label(f)}: 0.1 s after the cast`, 0.4);
const tl = u.skill.timeLeft;
assert.equal(h.b.dealDamage(null, u, { amount: 5000, type: 'true', sourceless: true }), 0, `${label(f)}: 无敌 in flight`);
const T = 5 / 3.5;
h.run(0.7);
approx(u.skill.timeLeft, tl, `${label(f)}: skill time paused`, 1e-6);
// the two latest deployed (护理 (9,5) and 德克萨斯 (10,5)) withdrawn and back on their tiles; 角峰 stays
for (const x of [pf, tx]) {
assert.ok(h.hooksOf('death').some((c) => c.unit === x && c.reason === 'retreat'), `${label(f)}: ${x.defId} withdrawn`);
assert.ok(x.alive && x.deploySeq > seq[2], `${label(f)}: ${x.defId} back at once`);
}
assert.ok(!h.hooksOf('death').some((c) => c.unit === yak), `${label(f)}: 角峰 (the first deployed) stays`);
assert.deepEqual([tx.tileR, tx.tileC, pf.tileR, pf.tileC], [10, 5, 9, 5], `${label(f)}: on their tiles`);
h.runUntil(() => !u.hidden, T);
assert.ok(!u.hidden, `${label(f)}: landed`);
approx(h.b.time - t0, 0.1 + T, `${label(f)}: after ${T.toFixed(2)} s of flight`, 0.1);
assert.deepEqual([u.tileR, u.tileC, u.dir], [10, 9, 'UP'], `${label(f)}: on the anchor's tile, facing its way`);
assert.ok(!a.alive && !a.removed && a.mem.docked, `${label(f)}: the anchor withdrawn (its piece kept)`);
assert.equal(u.mem.summonStock[ANCHOR], 0, `${label(f)}: stock emptied`);
assert.ok(u.skill.active, label(f));
approx(u.skill.timeLeft, tl, `${label(f)}: the remaining time kept`, 0.1);
assert.equal(sil.findBuff('talent:kalts2:regen')?.mods.hpRegen, 50, `${label(f)}: 赫默, 2.0 from her flight, entered T2`);
h.runUntil(() => !u.skill.active, 40);
h.run(5);
assert.ok(!a.alive, `${label(f)}: no anchor back after the skill (stock 0)`);
done(h);
}
});
test('the anchor link: she leaves the field before her S3 ⇒ the anchor on the field no longer moves her (kalts2_s_3[on_token_born] is lost with her); with S1 / S2 the anchor piece never takes the field', () => {
const { h, u, a } = field({ tier: 6, elite: true, skill: 2, anchor: { row: 10, col: 9 }, others: [{ uid: 3, chessId: YAK, row: 10, col: 5 }] });
const yak = h.unit(3);
assert.equal(u.mem.k2link, a);
h.b.kill(u, null);
h.step();
assert.equal(u.mem.k2link, null, 'link lost');
assert.ok(h.runUntil(() => u.alive, 90), 'she redeploys');
assert.ok(a.alive, 'the anchor still stands');
u.skill.gainSp(999);
yak.hp = yak.s.maxHp * 0.3;
assert.ok(h.runUntil(() => u.skill.active, 4));
h.run(3);
assert.deepEqual([u.tileR, u.tileC, u.hidden], [10, 4, false], 'no transport');
done(h);
for (const skill of [0, 1]) {
const r = field({ skill, anchor: { row: 10, col: 9 } });
r.h.run(2);
assert.ok(!r.a.alive && r.a.deferDeploy, `S${skill + 1}: the anchor stays off the field`);
done(r.h);
}
});
+377
View File
@@ -0,0 +1,377 @@
// test/content/op_monstr.test.js — the 自选 operator kit of Mon3tr (char_4179_monstr, 6★ 链愈师; kit
// server/sim/content/kits/ops/op-monstr.js) and her summon 重构体 (token_10050_monstr_prosts), fielded the production way (a
// DIY slot + its `diy` pick; the 重构体 as a token piece of hers) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no
// module) and elite (E2 Lv60, rank 7) with no module or XAH-X 记忆存档 at stage 1 (tier 5) / 3 (tier 6). Every number is read
// back from data/backups.json (the form of that slot status, the token's variant); the fidelity checklist of kits/README.md
// item by item.
// Run: node --test test/content/op_monstr.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 MONSTR = 'char_4179_monstr';
const PROSTS = 'token_10050_monstr_prosts';
const FORMS = BACKUPS.units[MONSTR].forms;
const TOKEN = BACKUPS.tokens[PROSTS];
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const XAHX = 'uniequip_002_monstr';
const S1 = 'skchr_monstr_1', S2 = 'skchr_monstr_2', S3 = 'skchr_monstr_3';
const statusKey = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusKey(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 variantOf = (tier, elite) => TOKEN.variants[`${MONSTR}@${statusKey(tier, elite)}`];
/** The trait / T1 blackboards a form fights with (module changes applied). */
const traitOf = (tier, elite, mod) => (elite && mod ? modOf(tier, mod).traitOverride.bb : formOf(tier, elite).trait.bb);
const t0Of = (tier, elite, mod) => (elite && mod && modOf(tier, mod).talentChanges.find((t) => t.talentIndex === 0)?.bb) || formOf(tier, elite).talents.find((t) => t.index === 0).bb;
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = {
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
enemy_biter: dummy('enemy_biter', { atk: 300, range: 1.2, bat: 1 }),
};
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, XAHX].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
const YAK = 'chess_char_1_02_a', ESTELLE = 'chess_char_1_12_a', SILENCE = 'chess_char_2_02_a', PERFUMER = 'chess_char_2_14_a';
/**
* A battle with Mon3tr as uid 1 at (10, 4) facing RIGHT (`prosts`: her piece as uid 2), plus `others`; `log` = her heals /
* damage in the order they happen.
*/
function field({ tier = 5, elite = false, mod = null, skill = 0, prosts = null, others = [], seed = 5, dp = 99 } = {}) {
const units = [{ uid: 1, diy: { slot: SLOT[tier], charId: MONSTR, skillIndex: skill, uniEquipId: mod }, elite, row: 10, col: 4 }];
if (prosts) units.push({ uid: 2, kind: 'token', tokenId: PROSTS, ownerUid: 1, row: prosts.row, col: prosts.col });
const log = [];
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpInit: dp, dpPerSec: 0, dpMax: 999 },
hooks: ['damaged', 'heal', 'skillStart', 'skillEnd', 'death', 'deploy', 'fatal'], captureNoisy: true,
units: [...units, ...others],
setup: (b) => {
b.on('heal', (c) => { if (!c.opts?.regen) log.push({ name: 'heal', ...c, t: b.time }); }, { priority: -1000 });
b.on('damaged', (c) => log.push({ name: 'damaged', ...c, t: b.time }), { priority: -1000 });
},
});
h.step();
return { h, u: h.unit(1), p: prosts ? h.unit(2) : null, log };
}
const healsOf = (log, u, from = 0) => log.slice(from).filter((c) => c.name === 'heal' && c.source === u);
const setHp = (x, r) => { x.hp = x.s.maxHp * r; };
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('Mon3tr in every 自选 form: her operator kit (all three skills authored), stats + module attributes, y-2, one main heal per attack (her own chain), blocks 1, ground-targetable, 罗德岛, no 特质; her 重构体: the form\'s token stats, 禁疗, block 0, taunt −1', () => {
assert.equal(OPERATOR_KITS[MONSTR], KITS[MONSTR]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const { h, u, p } = field({ tier, elite, mod, skill, prosts: { row: 10, col: 6 } });
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], [MONSTR, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.bat, u.base.cost], [form.stats.maxHp, form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 2.85, 18], `${label(f)}: stats`);
assert.deepEqual([u.profile.dmgType, u.profile.heal, u.profile.attack, u.def.subProf, u.s.blockCnt], ['heal', { mode: 'single' }, 'ranged', 'chainhealer', 1], `${label(f)}: 链愈师`);
assert.deepEqual(u.def.traitBb, traitOf(tier, elite, mod), `${label(f)}: the trait (XAH-X 0.85)`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-2`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.tokens, u.def.raw.nationId], [BACKUPS.diy.operators[MONSTR].bonds, [], [PROSTS], 'rhodes'], `${label(f)}: bonds / 特质 / summon`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: enemies target her`);
const ts = ((elite && mod && variantOf(tier, elite).byModule?.[mod]) || variantOf(tier, elite)).stats;
assert.deepEqual([p.base.maxHp, p.base.def, p.base.atk, p.base.cost, p.base.respawnTime, p.s.blockCnt, p.s.taunt], [ts.maxHp, ts.def, 0, 3, 15, 0, -1], `${label(f)}: 重构体 stats`);
assert.deepEqual([p.s.flags.noHeal, p.s.flags.noBlock, p.profile.noAttack, !!p.kit.generic, p.alive], [true, true, true, false, true], `${label(f)}: 禁疗, blocks nothing, no attack, on the field`);
done(h);
}
}
// E2 Lv1 1492 / 422 / 176, E2 Lv60 1653 / 492 / 206; XAH-X +40 / +30 → +65 / +40 ATK / DEF; the 重构体 4292 / 181 → 5048 / 208
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], [1492, 422, 1653, 492]);
assert.deepEqual([modOf(5, XAHX).attr, modOf(6, XAHX).attr], [{ atk: 40, def: 30 }, { atk: 65, def: 40 }]);
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(5, false).stats.def, variantOf(6, true).stats.maxHp], [4292, 181, 5048]);
assert.deepEqual([t0Of(5, true, XAHX).atk, t0Of(6, true, XAHX).atk, t0Of(6, true, XAHX)['attack@chain.extra_cnt'], t0Of(6, false, null).atk], [0.15, 0.25, 1, 0.15]);
});
test('a 自选 pick: Mon3tr is offered at tiers 5 and 6 (she has a kit) and a roster with her and XAH-X passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(MONSTR));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(MONSTR), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: MONSTR, skillIndex: 2, uniEquipId: XAHX } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: MONSTR, skillIndex: 2, uniEquipId: XAHX } } });
});
test('trait 链愈师: the heal jumps to the lowest-HP-ratio injured ally of the 3×3 around the last one healed, ×0.75 per jump (XAH-X ×0.85), 3 units healed; a full-HP unit and one two tiles away are skipped', () => {
for (const f of [[5, false, null], [6, true, null], [5, true, XAHX], [6, true, XAHX]]) {
const [tier, elite, mod] = f;
const step = traitOf(tier, elite, mod)['attack@chain.atk_scale'];
assert.equal(step, mod ? 0.85 : 0.75, label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }, { uid: 4, chessId: ESTELLE, row: 11, col: 6 }, { uid: 5, chessId: SILENCE, row: 12, col: 7 },
{ uid: 6, chessId: PERFUMER, row: 9, col: 7 }, { uid: 7, chessId: 'chess_char_1_11_a', row: 10, col: 6 }];
const { h, u, log } = field({ tier, elite, mod, skill: 2, others });
const [A, B, C, F, G] = [3, 4, 5, 6, 7].map((i) => h.unit(i));
setHp(A, 0.2); setHp(B, 0.5); setHp(C, 0.6); setHp(F, 0.3);
const n0 = log.length;
assert.ok(h.runUntil(() => healsOf(log, u, n0).length > 0, 4), label(f));
h.step();
const hs = healsOf(log, u, n0).filter((c) => c.t === healsOf(log, u, n0)[0].t);
assert.deepEqual(hs.map((c) => c.target), [A, B, C], `${label(f)}: A → B → C (G full, F two tiles off)`);
const atk = u.s.atk;
approx(hs[0].amount, atk, `${label(f)}: the main heal`);
approx(hs[1].amount, atk * step, `${label(f)}: ×${step}`);
approx(hs[2].amount, atk * step * step, `${label(f)}: ×${step}²`);
assert.ok(!healsOf(log, u, n0).some((c) => c.target === F || c.target === G), `${label(f)}: F / G not healed`);
done(h);
}
});
test('T1 自我修复 free link: her 重构体 in a chain uses no target of the count and the next jump keeps the amount; XAH-X stage 3: one more jump without decay', () => {
for (const f of [[5, false, null], [5, true, XAHX], [6, true, XAHX]]) {
const [tier, elite, mod] = f;
const step = traitOf(tier, elite, mod)['attack@chain.atk_scale'];
const extra = t0Of(tier, elite, mod)['attack@chain.extra_cnt'] ?? 0;
assert.equal(extra, tier === 6 && mod ? 1 : 0, label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }, { uid: 4, chessId: ESTELLE, row: 11, col: 6 }, { uid: 5, chessId: SILENCE, row: 12, col: 7 }];
const { h, u, p, log } = field({ tier, elite, mod, skill: 2, prosts: { row: 11, col: 5 }, others });
const [A, B, C] = [3, 4, 5].map((i) => h.unit(i));
setHp(A, 0.2); setHp(p, 0.3); setHp(B, 0.5); setHp(C, 0.6);
const n0 = log.length;
assert.ok(h.runUntil(() => healsOf(log, u, n0).length > 0, 4), label(f));
const hs = healsOf(log, u, n0).filter((c) => c.t === healsOf(log, u, n0)[0].t);
assert.deepEqual(hs.map((c) => c.target), [A, p, B, C], `${label(f)}: A → 重构体 → B → C (4 healed: the 重构体 is not counted)`);
const atk = u.s.atk;
approx(hs[1].amount, atk * step, `${label(f)}: the jump onto it decays`);
approx(hs[2].amount, atk * step, `${label(f)}: the next one does not`);
approx(hs[3].amount, atk * step * (extra ? 1 : step), `${label(f)}: then ${extra ? 'still no decay (下次跳跃也不衰减)' : 'it decays again'}`);
done(h);
}
});
test('T1 自我修复 — the 重构体: only her heals reach it (禁疗), it loses 8 HP every 0.1 s from 1 s after it lands, every ally of its 3×3 ATK +15 % (XAH-X stage 3 +25 %; not 2 tiles away); gone ⇒ back 15 s later for 3 DP while she stands; withdrawn when she leaves, her redeployment readies it', () => {
for (const f of [[5, false, null], [6, true, XAHX], [5, true, XAHX]]) {
const [tier, elite, mod] = f;
const atkUp = t0Of(tier, elite, mod).atk;
const others = [{ uid: 3, chessId: YAK, row: 11, col: 6 }, { uid: 4, chessId: ESTELLE, row: 12, col: 6 }, { uid: 5, chessId: SILENCE, row: 9, col: 3 }];
const { h, u, p } = field({ tier, elite, mod, skill: 0, prosts: { row: 10, col: 6 }, others, dp: 50 });
const yak = h.unit(3), tx = h.unit(4), sil = h.unit(5);
h.run(0.3);
approx(yak.s.atk, yak.base.atk * (1 + atkUp), `${label(f)}: 角峰 next to it ATK +${atkUp * 100} %`);
approx(u.s.atk, u.base.atk, `${label(f)}: Mon3tr (two tiles off) nothing`);
approx(tx.s.atk, tx.base.atk, `${label(f)}: 艾丝黛尔 (two rows off) nothing`);
assert.equal(p.findBuff('talent:monstr:prostsAtk'), null, `${label(f)}: not itself`);
// the 流失: none in the first second, then 80 per second
const hp0 = p.hp;
assert.equal(hp0, p.s.maxHp);
h.run(0.6);
assert.equal(p.hp, hp0, `${label(f)}: nothing before 1 s`);
h.run(1.0);
const lost = hp0 - p.hp;
assert.ok(lost > 60 && lost < 100, `${label(f)}: ≈80 HP in the next second (${lost})`);
assert.ok(h.hooksOf('damaged').some((c) => c.target === p && c.dmg?.tags?.includes('hpLoss')), `${label(f)}: a 流失`);
// 禁疗: 赫默 never heals it; Mon3tr does
setHp(p, 0.3);
h.run(6);
assert.equal(h.hooksOf('heal').filter((c) => c.source === sil && c.target === p).length, 0, `${label(f)}: no heal from 赫默`);
assert.ok(h.hooksOf('heal').some((c) => c.source === u && c.target === p), `${label(f)}: Mon3tr heals it`);
// destroyed: back 15 s later for 3 DP
const dp0 = h.b.players[0].dp;
h.b.kill(p, null);
h.run(14.5);
assert.ok(!p.alive, `${label(f)}: not before 15 s`);
h.run(0.7);
assert.ok(p.alive, `${label(f)}: back`);
assert.deepEqual([p.tileR, p.tileC], [10, 6], `${label(f)}: on its tile`);
approx(h.b.players[0].dp, dp0 - 3, `${label(f)}: 3 DP`);
// she leaves: withdrawn; it comes with her redeployment at once
h.b.kill(u, null);
h.step();
assert.ok(!p.alive && !p.removed, `${label(f)}: withdrawn with her`);
assert.ok(h.runUntil(() => u.alive, 90), `${label(f)}: she redeploys`);
assert.ok(h.runUntil(() => p.alive, 0.3), `${label(f)}: at once`);
done(h);
}
});
test('T2 战术协同: each heal of hers gives its target and her ASPD +20 for 10 s (no stacking: refreshed); under S2 ×1.8 / ×2.3 — the strongest effect stays', () => {
for (const f of [[5, false, null], [6, true, XAHX]]) {
const [tier, elite, mod] = f;
const t1 = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
const s2 = skillOf(tier, elite, S2);
assert.deepEqual([t1.attack_speed, t1.buff_duration, s2.bb.talent_scale], [20, 10, elite ? 2.3 : 1.8], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }];
const { h, u, log } = field({ tier, elite, mod, skill: 1, others });
const yak = h.unit(3);
setHp(yak, 0.3);
const n0 = log.length;
assert.ok(h.runUntil(() => healsOf(log, u, n0).length > 0, 4), label(f));
assert.deepEqual([yak.findBuff('talent:monstr:aspd')?.mods, u.findBuff('talent:monstr:aspd')?.mods], [{ aspd: 20 }, { aspd: 20 }], `${label(f)}: target and herself`);
approx(yak.findBuff('talent:monstr:aspd').timeLeft, 10, `${label(f)}: 10 s`, 0.05);
assert.equal(yak.s.aspd, yak.base.aspd + 20, label(f));
// S2 (an enemy in her y-2): ×talent_scale
h.spawn('enemy_dummy', { pos: [11, 6] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 2), `${label(f)}: S2`);
setHp(yak, 0.3);
const n1 = log.length;
assert.ok(h.runUntil(() => healsOf(log, u, n1).some((c) => c.target === yak), 6), label(f));
approx(yak.findBuff('talent:monstr:aspd').mods.aspd, 20 * s2.bb.talent_scale, `${label(f)}: ×${s2.bb.talent_scale}`);
approx(u.findBuff('talent:monstr:aspd').mods.aspd, 20 * s2.bb.talent_scale, `${label(f)}: herself too`);
done(h);
}
});
test('S1 策略:超压链接 (AUTO, attack SP 5 / 3, data DEFAULT — the next heal): that heal is 135 % / 170 % ATK and heals 4 units', () => {
for (const f of [[5, false, null], [6, true, XAHX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S1);
const step = traitOf(tier, elite, mod)['attack@chain.atk_scale'];
assert.deepEqual([sk.skillType, sk.spType, sk.spCost, sk.initSp, sk.bb.heal_scale, sk.bb['chain.max_target'], sk.trigger.rule],
['AUTO', 'INCREASE_WHEN_ATTACK', elite ? 3 : 5, 0, elite ? 1.7 : 1.35, 4, 'DEFAULT'], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }, { uid: 4, chessId: ESTELLE, row: 11, col: 6 }, { uid: 5, chessId: SILENCE, row: 12, col: 7 }, { uid: 6, chessId: PERFUMER, row: 12, col: 8 }];
const { h, u, log } = field({ tier, elite, mod, skill: 0, others });
const [A, B, C, D] = [3, 4, 5, 6].map((i) => h.unit(i));
assert.deepEqual([u.skill.rule, u.skill.spType, u.skill.spCost], ['DEFAULT', 'attack', sk.spCost], label(f));
u.skill.gainSp(999);
setHp(A, 0.2); setHp(B, 0.4); setHp(C, 0.5); setHp(D, 0.6);
const n0 = log.length;
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `${label(f)}: cast as she heals`);
const hs = healsOf(log, u, n0);
assert.deepEqual(hs.slice(0, 4).map((c) => c.target), [A, B, C, D], `${label(f)}: 4 units`);
const base = u.s.atk * sk.bb.heal_scale;
hs.slice(0, 4).forEach((c, i) => approx(c.amount, base * step ** i, `${label(f)}: link ${i}`));
assert.ok(!u.skill.active, `${label(f)}: spent on that heal`);
done(h);
}
});
test('S2 策略:超负荷 (MANUAL, data SKILL_RANGE on y-2: an enemy there; 30 s): her 重构体 in range is her heal target, even at full HP with nobody injured; 0.6 s after her attack healed it a second chain starts on it', () => {
for (const f of [[5, false, null], [6, true, XAHX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.duration, sk.trigger.rule, sk.rangeId, sk.spCost, sk.initSp], [30, 'SKILL_RANGE', 'y-2', 15, elite ? 8 : 7], label(f));
const others = [{ uid: 3, chessId: YAK, row: 11, col: 5 }, { uid: 4, chessId: ESTELLE, row: 12, col: 6 }];
const { h, u, p, log } = field({ tier, elite, mod, skill: 1, prosts: { row: 10, col: 5 }, others });
const yak = h.unit(3), tx = h.unit(4);
assert.equal(u.skill.rule, 'SKILL_RANGE');
u.skill.gainSp(999);
setHp(yak, 0.3);
h.run(2);
assert.equal(u.skill.activations, 0, `${label(f)}: an injured ally, no enemy: no cast`);
h.spawn('enemy_dummy', { pos: [9, 6] });
assert.ok(h.runUntil(() => u.skill.active, 0.5), `${label(f)}: an enemy in her y-2`);
approx(u.skill.timeLeft, 30, `${label(f)}: 30 s`, 0.05);
// 角峰 is lower, the 重构体 is still the target
setHp(p, 0.9); setHp(yak, 0.2);
let n0 = log.length;
assert.ok(h.runUntil(() => healsOf(log, u, n0).length > 0, 4), label(f));
const first = healsOf(log, u, n0);
assert.equal(first[0].target, p, `${label(f)}: the 重构体 first`);
const t0 = first[0].t;
// the second chain 0.6 s later, on it again
h.run(0.7);
const again = healsOf(log, u, n0).filter((c) => c.t > t0 + 0.5);
assert.ok(again.length > 0, `${label(f)}: a second chain`);
approx(again[0].t - t0, 0.6, `${label(f)}: 0.6 s later`, 0.06);
assert.equal(again[0].target, p, `${label(f)}: starting on the 重构体`);
approx(again[0].amount, u.s.atk, `${label(f)}: her ATK`);
// nobody injured: she heals the full-HP 重构体 anyway
setHp(yak, 1); setHp(tx, 1); setHp(u, 1); p.hp = p.s.maxHp;
n0 = log.length;
h.run(4);
assert.ok(healsOf(log, u, n0).some((c) => c.target === p), `${label(f)}: the full-HP 重构体 healed`);
done(h);
}
});
test('S3 策略:熔毁 (MANUAL, data DEFAULT, 25 s): refused without her 重构体; with it: it is withdrawn and she moves onto its tile — x-4, ATK +, interval 1.35 s, block 3, max HP +5000 (ratio kept), 流失 80/s; true damage on every ground enemy she blocks, a chain heal on herself (0.5 ATK, a free link) each attack; back at the end with SP 0, the 重构体 returns', () => {
for (const f of [[5, false, null], [6, true, XAHX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.bb.block_cnt, sk.bb.max_hp, sk.bb.damage_per_second, sk.bb['attack@heal_scale'], sk.rangeId, sk.trigger.rule],
[25, elite ? 2.8 : 2.5, -1.5, 2, 5000, 80, 0.5, 'x-4', 'DEFAULT'], label(f));
// refused without the 重构体 (destroyed: its piece waits)
const r0 = field({ tier, elite, mod, skill: 2, prosts: { row: 10, col: 6 }, others: [{ uid: 3, chessId: YAK, row: 11, col: 4 }] });
r0.h.b.kill(r0.p, null);
r0.u.skill.gainSp(999);
setHp(r0.h.unit(3), 0.3);
r0.h.run(3);
assert.equal(r0.u.skill.activations, 0, `${label(f)}: no cast without the 重构体`);
assert.ok(r0.u.skill.ready, `${label(f)}: still ready`);
done(r0.h);
const others = [{ uid: 3, chessId: YAK, row: 11, col: 4 }, { uid: 4, chessId: ESTELLE, row: 9, col: 7 }];
const { h, u, p, log } = field({ tier, elite, mod, skill: 2, prosts: { row: 10, col: 6 }, others, dp: 50 });
const yak = h.unit(3), tx = h.unit(4);
const hpRatio = 0.8;
setHp(u, hpRatio);
u.skill.gainSp(999);
setHp(yak, 0.3);
assert.ok(h.runUntil(() => u.skill.active, 4), label(f));
assert.ok(!p.alive && !p.removed, `${label(f)}: the 重构体 withdrawn (its piece waits)`);
assert.deepEqual([u.tileR, u.tileC, u.ground], [10, 6, true], `${label(f)}: on its tile (a ground tile)`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `${label(f)}: x-4`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
approx(u.s.interval, 2.85 - 1.5, `${label(f)}: 1.35 s`);
assert.equal(u.s.blockCnt, 3, `${label(f)}: block 3`);
approx(u.s.maxHp, u.base.maxHp + 5000, `${label(f)}: max HP +5000`);
approx(u.hpRatio, hpRatio, `${label(f)}: the ratio kept`, 1e-3);
// the 流失
const hpA = u.hp;
h.run(1);
approx(hpA - u.hp, 80, `${label(f)}: 80 per second`, 0.15);
// the blocked: two ground enemies on her tile, a flyer beside her
const g1 = h.spawn('enemy_dummy', { pos: [10, 6] });
const g2 = h.spawn('enemy_dummy', { pos: [10, 6] });
const fly = h.spawn('enemy_fly', { pos: [10, 7] });
h.step();
assert.deepEqual([g1.blockedBy, g2.blockedBy], [u, u], `${label(f)}: blocked`);
const n0 = log.length;
assert.ok(h.runUntil(() => log.slice(n0).some((c) => c.name === 'damaged' && c.source === u && c.target.side === 'enemy'), 3), label(f));
h.step();
const atkHits = log.slice(n0).filter((c) => c.name === 'damaged' && c.source === u && c.dmg?.isAttack);
const id = atkHits[0].dmg.attackId;
const one = atkHits.filter((c) => c.dmg.attackId === id);
assert.deepEqual(one.map((c) => c.target).sort((a, b) => a.id - b.id), [g1, g2].sort((a, b) => a.id - b.id), `${label(f)}: both blocked enemies, not the flyer`);
for (const c of one) { assert.equal(c.type, 'true', `${label(f)}: true damage`); approx(c.amount, u.s.atk, `${label(f)}: 100 % ATK`); }
assert.ok(!log.slice(n0).some((c) => c.target === fly), `${label(f)}: ground only`);
const self = log.slice(n0).find((c) => c.name === 'heal' && c.source === u && c.target === u);
assert.ok(self, `${label(f)}: the attack heals herself`);
approx(self.amount, u.s.atk * 0.5, `${label(f)}: 50 % ATK`);
// the end: back on her tile, SP 0, the 重构体 returns (its 15 s are long past) for 3 DP
const dp0 = h.b.players[0].dp;
h.runUntil(() => !u.skill.active, 30);
assert.deepEqual([u.tileR, u.tileC, u.skill.sp], [10, 4, 0], `${label(f)}: 【返回】, SP emptied`);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `${label(f)}: y-2 again`);
approx(u.s.maxHp, u.base.maxHp, `${label(f)}: max HP back`);
assert.ok(h.runUntil(() => p.alive, 0.5), `${label(f)}: the 重构体 back`);
assert.deepEqual([p.tileR, p.tileC], [10, 6], label(f));
approx(h.b.players[0].dp, dp0 - 3, `${label(f)}: 3 DP`);
assert.ok(tx.alive, label(f));
done(h);
}
});
test('S3 不死: a fatal blow leaves her at 1 HP (untargetable, blocking nothing), the skill ends on the next tick and she returns at ≤ 1 HP with 1 s of 不死; afterwards a fatal blow knocks her out', () => {
const { h, u, p } = field({ tier: 6, elite: true, mod: XAHX, skill: 2, prosts: { row: 10, col: 6 }, others: [{ uid: 3, chessId: YAK, row: 11, col: 4 }] });
u.skill.gainSp(999);
setHp(h.unit(3), 0.3);
assert.ok(h.runUntil(() => u.skill.active, 4));
assert.deepEqual([u.tileR, u.tileC], [10, 6]);
h.b.dealDamage(null, u, { amount: 1e7, type: 'true', sourceless: true });
assert.deepEqual([u.alive, u.hp, u.s.flags.untargetable, u.s.flags.noBlock, u.skill.active], [true, 1, true, true, true], 'held at 1 HP');
assert.ok(h.hooksOf('fatal').some((c) => c.unit === u && c.prevented));
h.step();
assert.ok(!u.skill.active, 'the skill ended');
assert.deepEqual([u.tileR, u.tileC], [10, 4], 'returned');
assert.ok(!u.s.flags.untargetable, 'targetable again');
h.b.dealDamage(null, u, { amount: 1e7, type: 'true', sourceless: true });
assert.ok(u.alive && u.hp <= 1, '1 s 不死 after the return');
h.run(1.1);
h.b.dealDamage(null, u, { amount: 1e7, type: 'true', sourceless: true });
assert.ok(!u.alive, 'then a knock-out');
assert.ok(!p.alive, 'her 重构体 off the field');
done(h);
});
+214 -98
View File
@@ -4495,48 +4495,48 @@
"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=kalts2/S1 5_diy2_a=kalts2/S2 5_diy1_a=kalts2/S3 5_diy2_b=kalts2/S1 5_diy1_b=kalts2/S2 5_diy2_b=kalts2/S3 6_diy1_a=kalts2/S1 6_diy2_a=kalts2/S2 6_diy1_a=kalts2/S3 6_diy2_b=kalts2/S1 6_diy1_b=kalts2/S2 6_diy2_b=kalts2/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_abyss",
"round": 12,
"round": 13,
"seed": 352330692,
"waveId": "act1autochess_h05",
"time": 193.8,
"ticks": 5814,
"waveId": "act1autochess_h06",
"time": 206.53,
"ticks": 6196,
"reason": "cleared",
"killed": 0,
"total": 126,
"total": 117,
"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":0,"total":117,"perfect":false,"leaks":117,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":0,"deaths":0,"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,1741.25,1,5,0,0,0,0,0,0],
["p1",2,"op","chess_char_5_diy2_a",1,null,1741.25,1,7,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,1741.25,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",1928.75,1,11,0,0,0,0,0,0],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1928.75,1,13,0,0,0,0,0,0],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1928.75,1,6,0,0,0,0,0,0],
["p1",7,"op","chess_char_6_diy1_a",0,null,1741.25,1,8,0,0,0,0,0,0],
["p1",8,"op","chess_char_6_diy2_a",1,null,1741.25,1,10,0,0,0,0,0,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,1741.25,1,12,0,0,0,0,0,0],
["p1",10,"op","chess_char_6_diy2_b",0,"none",1928.75,1,14,0,0,0,0,0,0],
["p1",11,"op","chess_char_6_diy1_b",1,"none",1928.75,1,3,0,0,0,0,0,0],
["p1",12,"op","chess_char_6_diy2_b",2,"none",1928.75,1,4,0,0,0,0,0,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_1166_dusbr_2":[39,0,39,0,0],"enemy_1169_duphlx_2":[57,0,57,0,0],"enemy_1172_dugago":[21,0,21,0,0]},
"hooks": [1,131,6196,0,0,0,0,0,0,117,0,0,0,8044,0,0,117,117,1,0,0,0,0],
"events": {"deploy":12,"fx":81,"leak":117,"spawn":131,"status":117},
"fx": {"shield":69,"takeoff":12},
"snaps": "ccc1569a 7999b522 dc7a568a ec4f6f74 10d4b114 031c504f 28e84098 6d770d64 f89924b0 b02f3be6 756308a2 e34ac8a6 ade9e07e ea1b33d3 d41b5c48 9780bf2f 2ea21fea 0955f226 69a9eb19 4bf17f0e a4ddcc48"
},
"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": "5_diy1_a=monstr/S1 5_diy2_a=monstr/S2 5_diy1_a=monstr/S3 5_diy2_b=monstr/S1 5_diy1_b=monstr/S2 5_diy2_b=monstr/S3 5_diy1_b=monstr/S1/002_monstr 5_diy2_b=monstr/S2/002_monstr 5_diy1_b=monstr/S3/002_monstr 6_diy1_a=monstr/S1 6_diy2_a=monstr/S2 6_diy1_a=monstr/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
@@ -4544,39 +4544,39 @@
"round": 12,
"seed": 4020340196,
"waveId": "act1autochess_h05",
"time": 203.3,
"ticks": 6099,
"reason": "cleared",
"killed": 90,
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 0,
"total": 90,
"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":0,"total":90,"perfect":false,"leaks":90,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":260434,"deaths":0,"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_5_diy1_a",0,null,1492,1,2,0,0,0,22788,83,13],
["p1",2,"op","chess_char_5_diy2_a",1,null,1492,1,3,0,0,0,26502,85,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,1492,1,4,0,0,0,19818,80,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",1553,1,6,0,0,35186,22540,89,13],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1653,1,9,0,0,23530,23492,88,1],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1653,1,10,0,0,23741,15632,75,0],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_monstr",1653,1,7,0,0,34018,23623,77,10],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_monstr",1653,1,11,0,0,23952,21553,90,1],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_monstr",1653,1,8,0,0,33583,25688,79,0],
["p1",10,"op","chess_char_6_diy1_a",0,null,1180.8,1,12,0,0,25286,20091,68,5],
["p1",11,"op","chess_char_6_diy2_a",1,null,1492,1,1,0,0,32246,21677,77,1],
["p1",12,"op","chess_char_6_diy1_a",2,null,1333.6,1,5,0,0,29462,17028,78,0]
],
"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,0,23,96508,0],"enemy_1229_darmy_2":[51,0,47,135033,0],"enemy_9007_acelem":[12,0,11,29462,0]},
"hooks": [1,102,6900,1621,1621,1392,1859,1285,0,81,47,47,0,490,0,9,90,81,1,0,0,40,0],
"events": {"atk":1937,"deploy":12,"dmg":1859,"fx":41,"heal":1284,"leak":81,"skill":94,"spawn":102,"status":77},
"fx": {"beam":1,"burst":40},
"snaps": "aaf95668 533f2864 617aef21 b49c1754 f4cd0e7c ebf0f57b eae6809f 4f4973cf 596a05d9 1dca3db2 1da78780 1de3ea69 025eae17 1dbbe670 2d4e063e c70c087c 5e75c144 c3441ead b47160c9 ba8a20ae 7fd02f4c 06627592 0f1deca3"
},
"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,
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=monstr/S1 6_diy2_b=monstr/S2 6_diy1_b=monstr/S3 6_diy2_b=monstr/S1/002_monstr 6_diy1_b=monstr/S2/002_monstr 6_diy2_b=monstr/S3/002_monstr 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_single_hard",
@@ -4586,76 +4586,192 @@
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 15,
"killed": 12,
"total": 36,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 95,
"rngDraws": 12,
"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":12,"total":36,"perfect":false,"leaks":24,"leaksUncounted":0,"coins":0,"dmg":20413,"bossDmg":0,"heal":86241,"deaths":24,"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",2,null,0,0,3,0,0,2128,0,0,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,5,0,0,3514,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,58,0,0,8014,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,52,0,0,40362,0,0,0],
["p1",5,"op","chess_char_6_diy1_b",0,"none",0,0,56,0,0,4632,10811,25,6],
["p1",6,"op","chess_char_6_diy2_b",1,"none",0,0,55,0,0,9541,15079,31,1],
["p1",7,"op","chess_char_6_diy1_b",2,"none",1653,1,61,0,0,17108,6354,18,0],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_002_monstr",0,0,63,0,0,6871,17616,33,8],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_002_monstr",0,0,59,0,0,4977,18039,33,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_monstr",0,0,53,0,0,18215,18341,34,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,54,0,5467,6222,0,22,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,62,0,14946,9667,0,31,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":[12,12,0,728,2121],"enemy_1005_yokai_3":[12,0,11,61742,1216],"enemy_9009_acfort":[12,0,12,68782,17076]},
"hooks": [1,63,6900,564,564,387,387,206,36,59,17,17,0,5611,0,0,36,23,1,24,0,0,0],
"events": {"atk":596,"deploy":25,"die":36,"dmg":387,"fx":43,"heal":206,"leak":23,"skill":34,"spawn":50,"status":23},
"fx": {"beam":43},
"snaps": "3868e4a8 32cfb83c 2a5847de 47aae40b e61ae62f 9e68b017 56f8e0fb 7161e2a8 b586e16a a930f382 79857faf da28a8f0 e339131f c06b18b5 294afda3 45c41b14 61ac71ac 121e2585 1b5fc8d7 2313cd11 9737892c 9c1c92ed 89ececb5"
},
"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": "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": "act2autochess_m03",
"modeId": "mode_multi_normal",
"round": 13,
"round": 12,
"seed": 500258608,
"waveId": "act1autochess_h06",
"time": 215.33,
"ticks": 6460,
"reason": "cleared",
"killed": 15,
"total": 123,
"waveId": "act1autochess_h05",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 9,
"total": 114,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 453,
"rngDraws": 84,
"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":9,"total":114,"perfect":false,"leaks":105,"leaksUncounted":0,"coins":0,"dmg":436116,"bossDmg":0,"heal":34284,"deaths":16,"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_5_diy1_a",2,null,0,0,113,0,3501,5208,0,65,1],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,115,0,12158,6742,0,62,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,132,1,14258,4587,0,15,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1678.71,1,139,3,202773,12739,0,64,5],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,15,2,84279,24799,0,157,4],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,112,3,66023,6822,0,47,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,137,0,8546,1133,0,61,2],
["p1",8,"op","chess_char_5_diy2_b",2,"none",736.41,1,10,0,29918,624,0,190,4],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,12,0,0,779,21853,53,4],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,14,0,0,0,12430,38,4],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,140,0,5333,1864,0,46,1],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,109,0,9326,2262,0,67,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_1166_dusbr_2":[45,7,38,10640,134702],"enemy_1169_duphlx_2":[51,2,49,15366,108013],"enemy_1172_dugago":[18,0,17,41553,193401]},
"hooks": [1,140,6900,1262,1262,1647,1623,91,25,129,31,28,0,31544,112,44,114,104,1,36,0,0,24],
"events": {"atk":1262,"deploy":23,"die":25,"dmg":1623,"fx":143,"heal":91,"leak":104,"skill":59,"spawn":129,"status":309},
"fx": {"aoe":49,"dodge":24,"dp":10,"revive":6,"slash":48,"stone":6},
"snaps": "4d1209f1 b3260553 22b87cb1 d677635e 68796d83 35ba34de 7424abb5 28710132 108c366c d224966c 9ee44ea0 842b17de 4cc18478 51b76f35 1d697235 995aaa08 73e7adc4 cbd87ae1 36bd1637 844fadc5 eefe7557 7c1300ca 97df0da6"
},
"diy-110": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_multi_hard",
"round": 12,
"seed": 2140781790,
"waveId": "act1autochess_h05",
"time": 228.73,
"ticks": 6862,
"reason": "cleared",
"killed": 8,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1091,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":8,"total":102,"perfect":false,"leaks":94,"leaksUncounted":0,"coins":0,"dmg":689939,"bossDmg":0,"heal":12640,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,132,4,31136,10089,0,33,1],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,135,2,108437,9449,0,43,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,137,0,17956,10806,0,26,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,136,0,32276,16982,0,41,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,3,1,60873,0,0,197,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,4,0,107377,0,0,200,3],
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,11,1,230227,0,0,84,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,13,0,53993,0,0,55,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,5,0,0,0,12640,25,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,10,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,12,0,20727,0,0,141,2],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,14,0,26937,0,0,132,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_1166_dusbr_2":[45,8,37,7484,315810],"enemy_1170_dushld_2":[27,0,27,17702,149257],"enemy_9011_acrefr":[30,0,30,22141,260852]},
"hooks": [1,137,6862,1034,1034,3454,3530,34,31,125,17,33,0,32054,9596,32,102,94,1,31,0,0,0],
"events": {"atk":1396,"deploy":33,"die":31,"dmg":3451,"fx":45,"heal":32,"leak":94,"skill":61,"spawn":116,"status":1142},
"fx": {"aoe":10,"displace":12,"spGain":23},
"snaps": "420918e6 2708d6ed 4f263102 03f8b055 7286f585 54a28451 a1d0d172 05faa2a9 b91da4d8 4f2742b8 65e41df2 60c24b73 b816e943 9d92e0d4 6b182fff e223813c 19b158a6 06277803 ed8ef216 d2c84879 eaf0cfa9 de2dd5ef feed0ad3"
},
"diy-111": {
"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_m01",
"modeId": "mode_single_normal",
"round": 13,
"seed": 452407399,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 87,
"total": 150,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 833,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":87,"total":150,"perfect":false,"leaks":63,"leaksUncounted":0,"coins":0,"dmg":1246786,"bossDmg":0,"heal":17719,"deaths":10,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,575.85,1,170,0,9397,7422,0,68,2],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",2613.1,1,172,1,14396,4249,0,106,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,163,1,15801,4587,0,27,1],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,159,8,87954,5712,0,32,3],
["p1",5,"op","chess_char_6_diy1_a",2,null,2638,1,173,1,9258,5276,0,33,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",3531,1,8,9,75994,14929,0,97,4],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,4,3,35846,0,0,95,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,5,25,130351,0,0,194,3],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,11,27,429294,0,0,78,2],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,13,12,438495,0,0,83,2],
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,17719,41,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,14,0,0,0,0,0,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],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1249_lysdb_2":[60,33,25,7724,541497],"enemy_1251_lysyta":[51,35,16,905,222652],"enemy_1329_cbshld":[39,19,20,33546,482637]},
"hooks": [1,173,6900,1185,1185,2901,2822,58,97,158,23,21,0,35181,0,88,150,61,1,101,0,0,19],
"events": {"atk":1444,"deploy":20,"die":97,"dmg":2822,"fx":198,"heal":58,"leak":61,"skill":44,"spawn":165,"status":8},
"fx": {"aoe":74,"dodge":19,"dp":13,"shieldBreak":60,"slash":32},
"snaps": "53a47bd2 4ea5e2f1 3fa6829f 372f41a8 4aa89456 4d7d2038 cd765f90 6163c3bf 249aaaf9 84674ef9 87186149 17a34f78 1cca8123 425cda0c e1cdcd06 83342211 c98e81a7 0404eb5a 38a6e8e8 beadf96d fc2760c7 9f7c829a 8af633fa"
},
"diy-112": {
"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_m02",
"modeId": "mode_multi_abyss",
"round": 13,
"seed": 3106580722,
"waveId": "act1autochess_h06",
"time": 214.33,
"ticks": 6430,
"reason": "cleared",
"killed": 1,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 55,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":1,"total":105,"perfect":false,"leaks":104,"leaksUncounted":0,"coins":0,"dmg":85268,"bossDmg":0,"heal":0,"deaths":24,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,81,0,22653,3730,0,12,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,1,1,34166,2494,0,15,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,131,0,8673,9232,0,26,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",670.54,1,130,0,13916,12685,0,13,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,128,0,2722,3480,0,8,0],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,67,0,3138,2900,0,8,1]
],
"enemies": {"enemy_1158_divman":[51,1,50,6114,52552],"enemy_1160_hvyslr_2":[24,0,24,5837,18182],"enemy_1161_tidmag_2":[30,0,30,22570,14534]},
"hooks": [1,131,6430,187,187,237,301,0,25,129,3,19,0,2487,56,15,105,104,1,25,0,4,1],
"events": {"atk":214,"deploy":26,"die":25,"dmg":287,"fx":16,"leak":104,"skill":39,"spawn":111,"status":94},
"fx": {"burst":4,"displace":10,"dodge":2},
"snaps": "0fdeb99b 2fc7ded4 445ab87f f27ef11c f56a9f26 af462f80 69da57fa afc2e0b4 cc31fcba faeb24a9 d364d537 b1a96f93 883e2bdd dab2271d ddcc8706 c6c8b95e 6073e7d8 f0b5b1b2 b030b896 39689147 6ed93392 e908d87f"
}
}
}