feedback5 O15: 自选 operator kits 森蚺, 斥罪 and 黍 — every skill, talent, trait and module (stage 1 / 3) at every form

- kits/ops/op-zumama.js (森蚺, 决战者): trait 阻回 unless blocking (charpack SpRecoveryStopped / SpRecovery); HES-X / HES-Y /
  RA-A turn it into an SP-recovery ratio (−0.8 / −0.999 / −1) while unblocked, HES-Y ATK / DEF +15 % blocking; RA-A's
  生息演算-only parts N/A (愈战愈勇 read from talentsBase); 勇冠三军 polled every 0.1 s (攻击力倍率 / 庇护); 愈战愈勇; S1 passive;
  S2 blockee stun past 抵抗; S3 hpRegenRatio + 5 s self-stun.
- kits/ops/op-judge.js (斥罪, 不屈者): 律法卫士 barrier (deploy / kill / S3, live 300 % cap, S2 ×(1 + shield_scale)); 荆棘环身
  thorns while barrier holds (the breaking hit too); UNY-X ×0.85 from blocked units, UNY-Y +8 % alone; S1 蓄力 (all SP per
  cast, ×1.8 + 3 s stun, attack-cycle reset); S2 no attack, 庇护, x-4 pulses at 0.9 + n s; S3 barrier / ATK / taunt.
- kits/ops/op-shu.js (黍, 守护者): 百谷长青 sown tiles (hpRegen + 庇护, S2 ×scale, GUA-X stage 3 born tile, cleared when she
  leaves); 天有四时 (professions, 4 【岁】 squad ATK + SP / 4 s); S1 as 塞雷娅 S1 (DEFAULT + an ally of x-4 at ≤ half); S2 heals
  of 2 on x-1; S3 range x-2 with Attack-else-Heal, the harvest aura and the 播种标记 teleport; GUA-X ×1.15 at ≤ 50 %. Her S3
  data rule TRY_SEARCH_ALLY_SKILL (no engine rule) is mapped onto SKILL_RANGE + allies on x-2 (reported).
- 庇护 in all three through battle.applyStrongest(unit, 'protect') (同名效果取最高 across these kits).
- kits/index.js: the three files appended to OPERATOR_KIT_FILES.
- test/content/op_zumama.test.js (8), op_judge.test.js (8), op_shu.test.js (9).
- docs/research/03-operators.md: 黍's trigger row (a 自选 pick now, the kit's mapping).
- golden: only diy moved — diy-049 … diy-052 now field 森蚺 / 斥罪 / 黍 and diy-053 … diy-058 are new (the 12-per-battle
  packing appends the new configurations; the prototype / reserve battles of 049 … 052 move to 055 … 058); diy-001 … 048
  and every other family unchanged.
This commit is contained in:
sganggs committed 2026-10-06 01:30:05 +08:00
1 parent fef4ea3d4b
commit 10bb226382
9 files changed
+1926 -99

No files matched your search

+314
View File
@@ -0,0 +1,314 @@
// test/content/op_judge.test.js — the 自选 operator kit of 斥罪 (char_4065_judge, 6★ 不屈者; kit
// server/sim/content/kits/ops/op-judge.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, UNY-X
// “通往未来的荆棘之路” or UNY-Y “无罪” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json
// (the form of that slot status); the fidelity checklist of kits/README.md item by item.
// Run: node --test test/content/op_judge.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const JUDGE = 'char_4065_judge';
const FORMS = BACKUPS.units[JUDGE].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const UNYX = 'uniequip_002_judge', UNYY = 'uniequip_003_judge';
const S1 = 'skchr_judge_1', S2 = 'skchr_judge_2', S3 = 'skchr_judge_3';
const BARRIER = 'talent:judge:barrier';
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = {
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_frail: dummy('enemy_frail', { hp: 10 }),
};
const ALLIES = { t_mate: chessRec({ id: 't_mate', skill: null, stats: { maxHp: 10000, atk: 0, def: 0, blockCnt: 0 } }),
t_medic: chessRec({ id: 't_medic', profession: 'MEDIC', skill: null, stats: { maxHp: 10000, atk: 500, def: 0, blockCnt: 0 } }) };
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, UNYX, UNYY].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** A battle with 斥罪 as uid 1 at (row, col) facing RIGHT, plus `others`. */
function field({ tier = 5, elite = false, mod = null, skill = 2, row = 10, col = 5, others = [], seed = 5, setup = null } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES, chess: ALLIES }, timeLimit: 900, autoFinish: false, seed, setup,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'heal', 'skillStart', 'skillEnd', 'statusApplied', 'kill'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: JUDGE, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
const barrier = (u) => u.findBuff(BARRIER)?.shield ?? 0;
const t0Of = (u) => u.def.raw.talents.find((t) => t.index === 0).bb;
const t1Of = (u) => u.def.raw.talents.find((t) => t.index === 1).bb;
const thorns = (h, u, target = null) => h.hooksOf('damaged').filter((c) => c.source === u && (c.dmg?.tags ?? []).includes('judge:thorns') && (!target || c.target === target));
/** Knock an enemy out with damage credited to her. */
function killOne(h, u) {
const e = h.spawn('enemy_frail', { pos: [12, 9] });
h.b.dealDamage(u, e, { amount: 1e6, type: 'true' });
assert.ok(!e.alive, 'knocked out');
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('斥罪 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 1-1 range, blocks 3, melee ground-only, 叙拉古, no 特质; 不屈者: no heal from others; triggers from the data', () => {
assert.equal(OPERATOR_KITS[JUDGE], KITS[JUDGE]);
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, others: [{ uid: 2, chessId: 't_medic', row: 11, col: 5 }] });
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], [JUDGE, 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.noHeal, u.base.bat], [3, 'melee', 'phys', false, true, 1.6], `${label(f)}: 不屈者`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['siracusaShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
const sk = form.skills[skill];
if (skill === 0) assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.skillType, u.skill.maxCharges], ['DEFAULT', 'DEFAULT', 'AUTO', 2], `${label(f)}: S1 AUTO`);
else assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.skillType], ['DEFAULT', 'TAKE_DAMAGE', 'MANUAL'], `${label(f)}: S${skill + 1} trigger (the 重装 exception)`);
assert.equal(u.skill.spType, skill === 2 ? 'hurt' : 'time', `${label(f)}: SP type`);
// 无法被友方角色治疗: a heal from another unit does nothing
u.hp = u.s.maxHp * 0.5;
assert.equal(h.b.heal(h.unit(2), u, 500), 0, `${label(f)}: no heal from others`);
done(h);
}
}
// zh_CN numbers: E2 Lv1 2960 / 683 / 474, E2 Lv60 3686 / 811 / 568; UNY-X +150/+35/+15 → +300/+50/+33, UNY-Y +60/+48 → +85/+70
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], [2960, 683, 3686, 811]);
assert.deepEqual([modOf(5, UNYX).attr, modOf(6, UNYX).attr, modOf(5, UNYY).attr, modOf(6, UNYY).attr],
[{ maxHp: 150, atk: 35, def: 15 }, { maxHp: 300, atk: 50, def: 33 }, { atk: 60, def: 48 }, { atk: 85, def: 70 }]);
});
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(JUDGE));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(JUDGE), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: JUDGE, skillIndex: 0, uniEquipId: UNYY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: JUDGE, skillIndex: 0, uniEquipId: UNYY } } });
});
test('T1 律法卫士: barrier 50 % max HP at every deployment (70 % UNY-X stage 3), +10 % per enemy she knocks out (12 %), never past 300 % of the current max HP; it absorbs before HP', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2 });
const t0 = t0Of(u);
const x3 = elite && mod === UNYX && tier === 6;
assert.deepEqual([t0.born_hp_ratio, t0.kill_hp_ratio, t0.max_hp_ratio], x3 ? [0.7, 0.12, 3] : [0.5, 0.1, 3], label(f));
approx(barrier(u), u.s.maxHp * t0.born_hp_ratio, `${label(f)}: at the deployment`);
killOne(h, u);
approx(barrier(u), u.s.maxHp * (t0.born_hp_ratio + t0.kill_hp_ratio), `${label(f)}: +1 kill`);
// it absorbs: HP untouched until it runs out
const hp0 = u.hp, e = h.spawn('enemy_dummy', { pos: [12, 9] });
h.b.dealDamage(e, u, { amount: 300, type: 'true' });
assert.equal(u.hp, hp0, `${label(f)}: absorbed`);
approx(barrier(u), u.s.maxHp * (t0.born_hp_ratio + t0.kill_hp_ratio) - 300, `${label(f)}: −300`);
// the cap: 300 % of the current max HP
u.findBuff(BARRIER).shield = u.s.maxHp * 2.95;
killOne(h, u);
approx(barrier(u), u.s.maxHp * 3, `${label(f)}: capped`);
killOne(h, u);
approx(barrier(u), u.s.maxHp * 3, `${label(f)}: no gain at the cap`);
// a max-HP change keeps the barrier; the cap follows the current max HP
h.b.addBuff(u, { key: 'test:hp', mods: { hpPct: -0.5 } });
const kept = barrier(u);
killOne(h, u);
assert.equal(barrier(u), kept, `${label(f)}: above the new cap: no gain, no cut`);
h.b.removeBuff(u, 'test:hp');
// a new deployment: a new barrier
h.b.retreat(u);
h.b.redeploy(u);
h.step();
approx(barrier(u), u.s.maxHp * t0.born_hp_ratio, `${label(f)}: again at the redeployment`);
done(h);
}
});
test('T2 荆棘环身: while she holds barrier, every damage from an enemy deals 50 % ATK arts back (58 % UNY-Y stage 3) — the hit that breaks it too; none without barrier, for a 流失, a 无来源 hit or an ally', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 't_mate', row: 12, col: 3 }] });
const t1 = t1Of(u);
assert.equal(t1.atk_scale, elite && mod === UNYY && tier === 6 ? 0.58 : 0.5, label(f));
const e = h.spawn('enemy_dummy', { pos: [12, 9] });
h.b.dealDamage(e, u, { amount: 100, type: 'phys' });
const t = thorns(h, u, e);
assert.equal(t.length, 1, `${label(f)}: one reflection`);
approx(t[0].amount, u.s.atk * t1.atk_scale, `${label(f)}: ${t1.atk_scale * 100} % ATK`);
assert.deepEqual([t[0].type, t[0].dmg.isAttack, t[0].dmg.canDodge], ['arts', false, false], `${label(f)}: arts, not an attack, undodgeable`);
h.b.loseHp(u, 50, { source: e });
h.b.dealDamage(e, u, { amount: 50, type: 'true', sourceless: true });
h.b.dealDamage(h.unit(2), u, { amount: 50, type: 'true' });
assert.equal(thorns(h, u).length, 1, `${label(f)}: no reflection for a 流失, a 无来源 hit or an ally`);
// the breaking hit still reflects; then none
u.findBuff(BARRIER).shield = 10;
h.b.dealDamage(e, u, { amount: 200, type: 'true' });
assert.equal(barrier(u), 0, `${label(f)}: broken`);
assert.equal(thorns(h, u).length, 2, `${label(f)}: the breaking hit reflects`);
h.b.dealDamage(e, u, { amount: 50, type: 'true' });
assert.equal(thorns(h, u).length, 2, `${label(f)}: no barrier, no thorns`);
done(h);
}
});
test('S1 一锤定音 (AUTO, data DEFAULT): the next attack adds 160 % / 180 % ATK arts; every cast spends all the SP; 蓄力 (twice the cost) also stuns 3 s and raises all her damage ×1.8 (thorns included), and reaching it restarts her attack cycle', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
let probe = null;
const { h, u } = field({ tier, elite, skill: 0, setup: (b) => {
// during her 蓄力 attack an enemy hits her: the reflection carries the ×1.8
b.on('hit', (c) => {
if (!probe || probe.done || c.source !== probe.u || !c.dmg?.isAttack) return;
probe.done = true;
b.dealDamage(probe.e, probe.u, { amount: 10, type: 'true' });
});
} });
assert.deepEqual([sk.spCost, sk.initSp, sk.maxChargeTime, sk.bb.atk_scale_2, sk.bb['judge_s_1_enhance_checker.atk_scale'], sk.bb.stun], elite ? [4, 0, 2, 1.8, 1.8, 3] : [5, 0, 2, 1.6, 1.8, 3], `T${tier}`);
// one charge: a plain cast
u.skill.gainSp(sk.spCost, 'init');
assert.equal(u.skill.charges, 1);
const e = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC + 1] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), `T${tier}: cast at her attack`);
assert.deepEqual([u.skill.charges, u.skill.sp], [0, 0], `T${tier}: all SP spent`);
const hits = () => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e);
h.step(2);
const gavel = hits().filter((c) => (c.dmg?.tags ?? []).includes('judge:gavel'));
assert.equal(gavel.length, 1, `T${tier}: one arts rider`);
approx(gavel[0].amount, u.s.atk * sk.bb.atk_scale_2, `T${tier}: ${sk.bb.atk_scale_2 * 100} % ATK arts`);
assert.deepEqual([gavel[0].type, gavel[0].dmg.isSkill], ['arts', true]);
assert.ok(!h.hooksOf('statusApplied').some((c) => c.target === e && c.status === 'stun'), `T${tier}: no stun without 蓄力`);
// 蓄力: the SP runs on to twice the cost; the attack cycle restarts the moment it gets there
h.b.kill(e, u);
h.step(2);
u.atkCd = 1.2;
u.skill.setSpTotal(2 * sk.spCost - 0.02);
assert.equal(u.skill.charges, 1);
assert.ok(h.runUntil(() => u.skill.charges === 2, 1), `T${tier}: the stack fills`);
h.step();
assert.equal(u.atkCd, 0, `T${tier}: attack cycle restarted`);
const e2 = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC + 1] });
probe = { u, e: e2, done: false };
assert.ok(h.runUntil(() => u.skill.activations === 2, 1), `T${tier}: 蓄力 cast`);
assert.deepEqual([u.skill.charges, u.skill.sp], [0, 0], `T${tier}: all SP spent`);
h.step(2);
const all = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e2);
const phys = all.find((c) => c.dmg?.isAttack);
const rider = all.find((c) => (c.dmg?.tags ?? []).includes('judge:gavel'));
approx(phys.amount, u.s.atk * 1.8, `T${tier}: the attack ×1.8`);
approx(rider.amount, u.s.atk * sk.bb.atk_scale_2 * 1.8, `T${tier}: the rider ×1.8`);
const th = thorns(h, u, e2);
assert.ok(th.length >= 1, `T${tier}: a reflection during the attack`);
approx(th[0].amount, u.s.atk * t1Of(u).atk_scale * 1.8, `T${tier}: thorns ×1.8`);
const st = h.hooksOf('statusApplied').find((c) => c.target === e2 && c.status === 'stun' && c.source === u);
assert.ok(st, `T${tier}: stun`);
approx(st.duration, sk.bb.stun, `T${tier}: ${sk.bb.stun} s`);
assert.equal(u.s.atkScaleMul, 1, `T${tier}: ×1.8 gone after the attack`);
done(h);
}
});
test('S2 坚心苦修 (data DEFAULT): 20 s without attacks, 40 % / 50 % 庇护, 90 % / 110 % ATK arts to every ground enemy on her x-4 at 0.9 s and every second after (20 pulses), barrier gains ×1.6 / ×1.8 meanwhile', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
const { h, u } = field({ tier, elite, skill: 1 });
assert.deepEqual([sk.duration, sk.bb.damage_resistance, sk.bb.atk_scale, sk.bb.shield_scale, sk.rangeId], elite ? [20, 0.5, 1.1, 0.8, 'x-4'] : [20, 0.4, 0.9, 0.6, 'x-4'], `T${tier}`);
u.skill.gainSp(999, 'init');
const inside = [h.spawn('enemy_dummy', { pos: [10, 6] }), h.spawn('enemy_dummy', { pos: [11, 4] }), h.spawn('enemy_dummy', { pos: [9, 5] })];
const fly = h.spawn('enemy_fly', { pos: [9, 6] }), out = h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in range`);
const t0 = h.hooksOf('skillStart').find((c) => c.unit === u).t;
const pb = u.findBuff('protect');
approx(pb.data.value, sk.bb.damage_resistance, `T${tier}: 庇护`);
const n0 = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack).length;
// the barrier gain of a kill ×(1 + shield_scale)
const b0 = barrier(u);
killOne(h, u);
approx(barrier(u) - b0, u.s.maxHp * t0Of(u).kill_hp_ratio * (1 + sk.bb.shield_scale), `T${tier}: kill barrier ×${1 + sk.bb.shield_scale}`);
assert.ok(h.runUntil(() => !u.skill.active, sk.duration + 1), `T${tier}: ends`);
const pulses = h.hooksOf('damaged').filter((c) => c.source === u && (c.dmg?.tags ?? []).includes('judge:atonement'));
const times = [...new Set(pulses.map((c) => Math.round((c.t - t0) * 100) / 100))];
assert.equal(times.length, 20, `T${tier}: 20 pulses`);
approx(times[0], 0.9, `T${tier}: first at 0.9 s`, 0.05);
approx(times[1] - times[0], 1, `T${tier}: then every second`, 0.05);
for (const e of inside) assert.equal(pulses.filter((c) => c.target === e).length, 20, `T${tier}: ${e.tileR},${e.tileC} hit 20 times`);
for (const c of pulses.slice(0, 3)) {
approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`);
assert.deepEqual([c.type, c.dmg.isSkill, c.dmg.isAttack], ['arts', true, false]);
}
assert.ok(!pulses.some((c) => c.target === fly || c.target === out), `T${tier}: no flyer, nothing outside x-4`);
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack && c.t > t0 + 0.01 && c.t < t0 + sk.duration - 0.01).length - n0, 0, `T${tier}: no attack while it runs`);
assert.equal(u.findBuff('protect'), null, `T${tier}: 庇护 gone`);
done(h);
}
});
test('S3 披荆斩棘 (data DEFAULT, 受击回复 27 / 24 SP — absorbed hits count): barrier +70 % / +100 % max HP (to the cap), 30 s ATK +260 % / +320 %, interval 1.6 + 0.9 s, taunt +1', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const { h, u } = field({ tier, elite, skill: 2 });
assert.deepEqual([sk.duration, sk.bb.hp_ratio, sk.bb.atk, sk.bb.base_attack_time, sk.bb.taunt_level, sk.spCost, sk.initSp, sk.spType], elite ? [30, 1, 3.2, 0.9, 1, 24, 0, 'INCREASE_WHEN_TAKEN_DAMAGE'] : [30, 0.7, 2.6, 0.9, 1, 27, 0, 'INCREASE_WHEN_TAKEN_DAMAGE'], `T${tier}`);
const src = h.spawn('enemy_dummy', { pos: [12, 9] });
for (let i = 0; i < sk.spCost - 1; i++) h.b.dealDamage(src, u, { amount: 1, type: 'true' });
assert.equal(u.skill.sp, sk.spCost - 1, `T${tier}: one SP per hit, the barrier absorbing`);
assert.equal(u.hp, u.s.maxHp, `T${tier}: HP untouched`);
h.b.dealDamage(src, u, { amount: 1, type: 'true' });
assert.ok(u.skill.ready, `T${tier}: ready`);
h.run(1);
assert.equal(u.skill.activations, 0, `T${tier}: no enemy in range, no cast`);
const b0 = barrier(u);
h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in range`);
approx(barrier(u), Math.min(u.s.maxHp * 3, b0 + u.s.maxHp * sk.bb.hp_ratio), `T${tier}: +${sk.bb.hp_ratio * 100} % max HP`, 1e-3);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
approx(u.s.interval, 2.5, `T${tier}: 1.6 + 0.9 s`);
assert.equal(u.s.taunt, 1, `T${tier}: taunt +1`);
// the cap holds for the skill's barrier too
h.b.retreat(u);
h.b.redeploy(u);
h.step();
u.findBuff(BARRIER).shield = u.s.maxHp * 2.5;
u.skill.gainSp(999, 'init');
assert.ok(h.runUntil(() => u.skill.active, 5));
approx(barrier(u), u.s.maxHp * 3, `T${tier}: capped at 300 %`);
assert.ok(h.runUntil(() => !u.skill.active, sk.duration + 1));
approx(u.s.interval, 1.6, `T${tier}: interval back`);
assert.equal(u.s.taunt, 0, `T${tier}: taunt back`);
done(h);
}
});
test('UNY-X “通往未来的荆棘之路”: damage from an enemy she blocks ×0.85 (stages 1 and 3); UNY-Y “无罪”: ATK / DEF +8 % with no allied operator on the 8 tiles around her (a summon does not count)', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const others = [{ uid: 2, chessId: 't_mate', row: 12, col: 8 }, { uid: 3, chessId: 'chess_char_3_19_a', row: 12, col: 3 },
{ uid: 4, kind: 'token', tokenId: 'token_10028_vigil_wolf', ownerUid: 3, row: 11, col: 6 }];
const { h, u } = field({ tier, elite, mod, skill: 2, others });
const x = elite && mod === UNYX, y = elite && mod === UNYY;
if (x) assert.equal(u.def.raw.trait.bb.damage_scale, 0.85, label(f));
if (y) assert.deepEqual(u.def.raw.trait.bb, { atk: 0.08, def: 0.08 }, label(f));
u.findBuff(BARRIER).shield = 0;
const blocked = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC] });
assert.ok(h.runUntil(() => blocked.blockedBy === u, 1));
const free = h.spawn('enemy_dummy', { pos: [12, 9] });
const take = (src) => { u.hp = u.s.maxHp; return h.b.dealDamage(src, u, { amount: 500, type: 'true' }); };
approx(take(blocked), x ? 425 : 500, `${label(f)}: from the blocked enemy`);
approx(take(free), 500, `${label(f)}: from another`);
// the wolf (a summon) is on the 8 around her: still alone
h.step(2);
assert.equal(!!u.findBuff('trait:judge:alone'), y, `${label(f)}: alone (a summon next to her)`);
if (y) approx(u.s.atk, u.base.atk * 1.08, `${label(f)}: ATK +8 %`);
h.b.relocate(h.unit(2), 9, 4); // an operator on her diagonal
h.step(2);
assert.equal(u.findBuff('trait:judge:alone'), null, `${label(f)}: not alone`);
done(h);
}
});
+370
View File
@@ -0,0 +1,370 @@
// test/content/op_shu.test.js — the 自选 operator kit of 黍 (char_2025_shu, 6★ 守护者; kit
// server/sim/content/kits/ops/op-shu.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or GUA-X
// “钦天司时” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json (the form of that slot
// status); the fidelity checklist of kits/README.md item by item.
// Run: node --test test/content/op_shu.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
import { 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 SHU = 'char_2025_shu';
const FORMS = BACKUPS.units[SHU].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const GUAX = 'uniequip_002_shu';
const S1 = 'skchr_shu_1', S2 = 'skchr_shu_2', S3 = 'skchr_shu_3';
const FARM = 'talent:shu:farm';
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = {
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 1, mass: 0 }),
};
const ally = (id, o = {}) => chessRec({ id, skill: null, ...o, stats: { maxHp: 10000, atk: 0, def: 0, blockCnt: 0, respawnTime: 5, ...(o.stats || {}) } });
const ALLIES = {
t_a: ally('t_a'), t_b: ally('t_b'), t_c: ally('t_c'), t_d: ally('t_d'),
t_war: ally('t_war', { profession: 'WARRIOR' }), t_war2: ally('t_war2', { profession: 'WARRIOR' }), t_war3: ally('t_war3', { profession: 'WARRIOR' }),
t_med: ally('t_med', { profession: 'MEDIC' }),
t_sp: chessRec({ id: 't_sp', profession: 'WARRIOR', stats: { maxHp: 10000, atk: 0, def: 0, blockCnt: 0 },
skill: { skillId: 'sk_t_sp', spType: 'INCREASE_WHEN_ATTACK', spCost: 99, initSp: 0, duration: 5 } }),
};
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, GUAX].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** A battle with 黍 as uid 1 at (row, col) facing RIGHT, plus `others`. */
function field({ tier = 5, elite = false, mod = null, skill = 2, row = 10, col = 5, others = [], seed = 5, enemies = [] } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES, chess: ALLIES }, timeLimit: 900, autoFinish: false, seed, enemies,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'heal', 'skillStart', 'skillEnd', 'statusApplied', 'attack'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: SHU, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
const healsBy = (h, u, f = () => true) => h.hooksOf('heal').filter((c) => c.source === u && !c.opts?.regen && f(c));
const sownOf = (u) => u.mem.shuSown ?? new Set();
const key = (r, c) => r * COLS + c;
/** GUA-X on this form: ×heal_scale on a target at or below hp_ratio. */
const guax = (u, ratio) => (u.def.raw.trait.bb.heal_scale && ratio <= u.def.raw.trait.bb.hp_ratio + 1e-9 ? u.def.raw.trait.bb.heal_scale : 1);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('黍 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, range 0-1, blocks 3, melee ground-only, 炎, no 特质; triggers (S3: TRY_SEARCH_ALLY_SKILL → SKILL_RANGE + allies on x-2)', () => {
assert.equal(OPERATOR_KITS[SHU], KITS[SHU]);
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], [SHU, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.res], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 10], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [3, 'melee', 'phys', false, 1.2], `${label(f)}: 守护者`);
assert.deepEqual(u.liveRangeGrid, [[0, 0]], `${label(f)}: 0-1`);
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`);
const sk = form.skills[skill];
if (skill === 0) assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.skillType, u.skill.triggerAllies, u.skill.triggerHpAtMost], ['DEFAULT', 'DEFAULT', 'AUTO', true, 0.5], `${label(f)}: S1`);
if (skill === 1) assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.skillType, u.skill.triggerAllies], ['DEFAULT', 'TAKE_DAMAGE', 'MANUAL', false], `${label(f)}: S2 (the 重装 exception)`);
if (skill === 2) {
assert.deepEqual([sk.trigger.rule, sk.trigger.rawRule, sk.skillType], ['TRY_SEARCH_ALLY_SKILL', 'TRY_SEARCH_ALLY_SKILL', 'MANUAL'], `${label(f)}: S3 data rule`);
assert.deepEqual([u.skill.rule, u.skill.triggerAllies, u.skill.triggerGrid], ['SKILL_RANGE', true, sk.rangeGrid], `${label(f)}: S3 engine rule`);
}
done(h);
}
}
// zh_CN numbers: E2 Lv1 2313 / 392 / 493, E2 Lv60 2910 / 450 / 565; GUA-X +160/+30/+30 → +270/+50/+50
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], [2313, 392, 2910, 450]);
assert.deepEqual([modOf(5, GUAX).attr, modOf(6, GUAX).attr], [{ maxHp: 160, atk: 30, def: 30 }, { maxHp: 270, atk: 50, def: 50 }]);
});
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(SHU));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(SHU), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: SHU, skillIndex: 2, uniEquipId: GUAX } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: SHU, skillIndex: 2, uniEquipId: GUAX } } });
});
test('S1 化被草木 (AUTO, DEFAULT + an ally of x-4 at ≤ half HP): her attack heals the lowest such ally for 130 % / 150 % ATK instead (×1.15 GUA-X); 1 / 2 charges; nothing for allies above half or outside x-4', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, GUAX]]) {
const sk = skillOf(tier, elite, S1);
const others = [{ uid: 2, chessId: 't_a', row: 11, col: 4 }, { uid: 3, chessId: 't_b', row: 9, col: 5 }, { uid: 4, chessId: 't_c', row: 12, col: 5 }];
const { h, u } = field({ tier, elite, mod, skill: 0, others });
assert.deepEqual([sk.bb.heal_scale, sk.maxChargeTime, sk.spCost, sk.rangeId], elite ? [1.5, 2, 5, 'x-4'] : [1.3, 1, 5, 'x-4'], `T${tier}`);
const [a, b, c] = [h.unit(2), h.unit(3), h.unit(4)];
const e = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC] });
u.skill.gainSp(999, 'init');
a.hp = a.s.maxHp * 0.6; b.hp = b.s.maxHp * 0.55; c.hp = c.s.maxHp * 0.1;
h.run(3);
assert.equal(u.skill.activations, 0, `T${tier}: nobody of x-4 at ≤ half (the one at 10 % is outside)`);
assert.ok(h.hooksOf('damaged').some((x) => x.source === u && x.target === e), `T${tier}: she attacks meanwhile`);
a.hp = a.s.maxHp * 0.3; b.hp = b.s.maxHp * 0.45;
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), `T${tier}: cast at her attack`);
const first = healsBy(h, u)[0];
assert.equal(first.target, a, `T${tier}: the lowest`);
approx(first.amount, u.s.atk * sk.bb.heal_scale * guax(u, 0.3), `T${tier}: ${sk.bb.heal_scale * 100} % ATK`, 1e-3);
const t0 = h.hooksOf('skillStart').find((x) => x.unit === u).t;
assert.ok(!h.hooksOf('damaged').some((x) => x.source === u && x.target === e && Math.abs(x.t - t0) < 1e-9), `T${tier}: that attack was the heal`);
assert.ok(sownOf(u).has(key(a.tileR, a.tileC)), `T${tier}: the heal sowed`);
if (elite) {
a.hp = a.s.maxHp;
assert.ok(h.runUntil(() => u.skill.activations === 2, 5), 'the second charge on the next attack');
assert.equal(healsBy(h, u)[1].target, b, 'the ally still at ≤ half');
}
done(h);
}
});
test('S2 嘉禾盈仓 (data DEFAULT: casts at her attack on an enemy): 25 s, her attacks heal up to 2 injured allies of x-1 for ATK (+60 % / +90 %), block 3 + 1, interval 1.2 + 1.3 s, range x-1 meanwhile; T1 ×1.3 / ×1.4', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, GUAX]]) {
const sk = skillOf(tier, elite, S2);
const others = [{ uid: 2, chessId: 't_a', row: 10, col: 7 }, { uid: 3, chessId: 't_b', row: 12, col: 5 }, { uid: 4, chessId: 't_c', row: 9, col: 4 }, { uid: 5, chessId: 't_d', row: 12, col: 7 }];
const { h, u } = field({ tier, elite, mod, skill: 1, others });
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.block_cnt, sk.bb.base_attack_time, sk.bb['attack@max_target'], sk.bb.extra_extend_scale, sk.rangeId], elite ? [25, 0.9, 1, 1.3, 2, 1.4, 'x-1'] : [25, 0.6, 1, 1.3, 2, 1.3, 'x-1'], `T${tier}`);
const [a, b, c, d] = [h.unit(2), h.unit(3), h.unit(4), h.unit(5)];
u.skill.gainSp(999, 'init');
a.hp = a.s.maxHp * 0.4; b.hp = b.s.maxHp * 0.7; c.hp = c.s.maxHp * 0.9; d.hp = d.s.maxHp * 0.2;
h.run(2);
assert.equal(u.skill.activations, 0, `T${tier}: injured allies alone do not cast it`);
const e = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast at her attack`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: x-1 while it runs`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
assert.equal(u.s.blockCnt, 4, `T${tier}: block 3 + 1`);
approx(u.s.bat, 2.5, `T${tier}: base attack time 1.2 + 1.3 s`); // (T2: four WARRIOR here ⇒ ASPD +12 on top)
const t0 = h.hooksOf('skillStart').find((x) => x.unit === u).t;
assert.ok(h.runUntil(() => healsBy(h, u, (x) => x.t >= t0 - 1e-9).length >= 2, 4), `T${tier}: a heal attack (the cast's own attack)`);
const hs = healsBy(h, u, (x) => x.t >= t0 - 1e-9);
assert.deepEqual(hs.slice(0, 2).map((x) => x.target.id).sort(), [a.id, b.id].sort(), `T${tier}: the two lowest of x-1 (the one at 20 % is outside)`);
for (const x of hs.slice(0, 2)) approx(x.amount, u.s.atk * guax(u, x.target === a ? 0.4 : 0.7), `T${tier}: ATK`, 1e-3);
assert.ok(!h.hooksOf('damaged').some((x) => x.source === u && x.target === e && x.t > t0 + 0.01), `T${tier}: no attack on the enemy`);
// T1 on her sown tiles ×extra_extend_scale while it runs
h.run(0.3);
const fb = a.findBuff(FARM);
assert.ok(fb, `T${tier}: the healed ally stands on a sown tile`);
const t0b = u.def.raw.talents.find((t) => t.index === 0).bb;
approx(fb.mods.hpRegen, t0b.hp_recovery_per_sec * sk.bb.extra_extend_scale, `T${tier}: regeneration ×${sk.bb.extra_extend_scale}`);
approx(a.findBuff('protect').data.value, t0b.damage_resistance * sk.bb.extra_extend_scale, `T${tier}: 庇护 ×${sk.bb.extra_extend_scale}`);
assert.ok(h.runUntil(() => !u.skill.active, sk.duration + 1), `T${tier}: ends`);
h.run(0.6);
assert.deepEqual(u.liveRangeGrid, [[0, 0]], `T${tier}: back to 0-1`);
approx(a.findBuff(FARM).mods.hpRegen, t0b.hp_recovery_per_sec, `T${tier}: regeneration back to ×1`);
approx(u.s.bat, 1.2, `T${tier}: attack time back`);
done(h);
}
});
test('S3 离离枯荣 (SKILL_RANGE + an injured ally of x-2 — no enemy needed, an enemy alone does not cast it): range x-2, ATK +22 % / +34 %, every attack on an enemy also heals the most injured other ally for ATK; with no enemy her attack heals (herself included)', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, GUAX]]) {
const sk = skillOf(tier, elite, S3);
const others = [{ uid: 2, chessId: 't_a', row: 12, col: 5 }, { uid: 3, chessId: 't_b', row: 12, col: 9 }];
const { h, u } = field({ tier, elite, mod, skill: 2, others });
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.e_atk, sk.bb.e_attack_speed, sk.bb.max_distance, sk.rangeId], elite ? [30, 0.34, 0.2, 20, 3, 'x-2'] : [30, 0.22, 0.15, 15, 4, 'x-2'], `T${tier}`);
const [a, far] = [h.unit(2), h.unit(3)];
u.skill.gainSp(999, 'init');
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
far.hp = far.s.maxHp * 0.2;
h.run(3);
assert.equal(u.skill.activations, 0, `T${tier}: an enemy and an injured ally outside x-2 do not cast it`);
a.hp = a.s.maxHp * 0.6;
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast for the injured ally of x-2`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: x-2 while it runs`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
const t0 = h.hooksOf('skillStart').find((x) => x.unit === u).t;
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((x) => x.source === u && x.target === e && x.t > t0), 3), `T${tier}: she strikes the enemy 2 tiles ahead`);
h.step();
const hit = h.hooksOf('damaged').find((x) => x.source === u && x.target === e && x.t > t0);
assert.deepEqual([hit.type, hit.dmg.isAttack], ['phys', true]);
const hh = healsBy(h, u, (x) => x.t >= hit.t - 1e-9);
assert.equal(hh[0]?.target, a, `T${tier}: the attack heals the injured ally`);
approx(hh[0].amount, u.s.atk, `T${tier}: for ATK`, 1e-3);
// she alone injured: an enemy attack heals nobody; with no enemy her attack heals her
a.hp = a.s.maxHp;
u.hp = u.s.maxHp * 0.7;
const n0 = healsBy(h, u).length;
h.run(3);
assert.equal(healsBy(h, u).length, n0, `T${tier}: the attack-heal is for the others`);
h.b.kill(e, u);
assert.ok(h.runUntil(() => healsBy(h, u).length > n0, 3), `T${tier}: no enemy: a heal attack`);
assert.equal(healsBy(h, u)[n0].target, u, `T${tier}: on herself`);
assert.ok(h.hooksOf('attack').some((x) => x.attacker === u && x.targets[0] === u), `T${tier}: an attack (ASPD / 缴械 count)`);
assert.ok(h.runUntil(() => !u.skill.active, sk.duration), `T${tier}: ends`);
assert.deepEqual(u.liveRangeGrid, [[0, 0]], `T${tier}: back to 0-1`);
done(h);
}
});
test('S3 离离枯荣: a ground enemy on a sown tile ⇒ the operators of her range ATK +15 % / +20 % and ASPD +15 / +20 (no summon); a ground enemy entering a sown tile is put back there once it is max_distance (4 / 3) tiles away — until the skill ends', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const others = [{ uid: 2, chessId: 't_a', row: 11, col: 7 }, { uid: 3, chessId: 'chess_char_3_19_a', row: 12, col: 3 },
{ uid: 4, kind: 'token', tokenId: 'token_10028_vigil_wolf', ownerUid: 3, row: 11, col: 4 }];
const { h, u } = field({ tier, elite, skill: 2, others });
const a = h.unit(2), wolf = h.unit(4);
// sow row 9 cols 6-8 around (9, 7) with a heal of hers (an ally there for the moment)
h.b.relocate(a, 9, 7);
h.b.heal(u, a, 1);
h.b.relocate(a, 11, 7);
for (const [r, c] of [[9, 6], [9, 7], [9, 8], [10, 7]]) assert.ok(sownOf(u).has(key(r, c)), `T${tier}: ${r},${c} sown`);
a.hp = a.s.maxHp * 0.5;
u.skill.gainSp(999, 'init');
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast`);
h.step();
assert.equal(a.findBuff('skill:shu:harvest'), null, `T${tier}: no enemy on a sown tile yet`);
const w = h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(h.runUntil(() => sownOf(u).has(key(Math.round(w.y), Math.round(w.x))), 15), `T${tier}: it reaches the sown tiles`);
h.step(2);
const hb = a.findBuff('skill:shu:harvest');
assert.ok(hb, `T${tier}: an operator of her range`);
assert.deepEqual(hb.mods, { atkPct: sk.bb.e_atk, aspd: sk.bb.e_attack_speed }, `T${tier}: +${sk.bb.e_atk * 100} % / +${sk.bb.e_attack_speed}`);
assert.ok(u.findBuff('skill:shu:harvest'), `T${tier}: herself`);
assert.equal(wolf.findBuff('skill:shu:harvest'), null, `T${tier}: not a summon`);
const mark = u.mem.shuMarks.get(w.id);
assert.deepEqual([mark.r, mark.c], [9, 8], `T${tier}: marked with the first sown tile it entered`);
let farthest = 8, ported = false;
for (let i = 0; i < 600 && !ported; i++) {
h.step();
const c = Math.round(w.x);
if (c < farthest) farthest = c;
if (c === 8 && farthest < 8) ported = true;
}
assert.ok(ported, `T${tier}: put back on its sown tile`);
// the tile at max_distance is never seen after a tick: it is put back the tick it enters it
assert.equal(farthest, 8 - sk.bb.max_distance + 1, `T${tier}: on entering the tile ${sk.bb.max_distance} tiles away`);
assert.ok(h.eventsOf('fx').some((x) => x[1] === 'teleport'), `T${tier}: teleport fx`);
// after the skill: no more teleports
u.skill.end('test');
h.step();
assert.equal(u.mem.shuMarks, null, `T${tier}: the marks end with the skill`);
let reachedGoal = false;
for (let i = 0; i < 900 && !reachedGoal; i++) { h.step(); if (!w.alive || Math.round(w.x) <= 8 - sk.bb.max_distance - 1) reachedGoal = true; }
assert.ok(reachedGoal, `T${tier}: it walks on`);
done(h);
}
});
test('T1 百谷长青: her heals sow the healed unit\'s tile and the 4 next to it (walkable / deployable tiles only); units on them regenerate 70 / s (80 GUA-X stage 3) and take 10 % (15 %) 庇护 — a 无法被治疗 unit and summons too; all gone when she leaves; GUA-X stage 3 sows her tile at the deployment', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const others = [{ uid: 2, chessId: 't_a', row: 9, col: 5 }, { uid: 3, chessId: 't_b', row: 12, col: 9 }, { uid: 4, chessId: 'chess_char_3_19_a', row: 12, col: 3 },
{ uid: 5, kind: 'token', tokenId: 'token_10028_vigil_wolf', ownerUid: 4, row: 10, col: 6 }];
const { h, u } = field({ tier, elite, mod, skill: 2, others });
const t0 = u.def.raw.talents.find((t) => t.index === 0);
const x3 = elite && mod === GUAX && tier === 6;
assert.deepEqual([t0.bb.hp_recovery_per_sec, t0.bb.damage_resistance, t0.bbStr?.born_range_id ?? null], x3 ? [80, 0.15, '0-1'] : [70, 0.1, null], label(f));
assert.equal(sownOf(u).has(key(10, 5)), x3, `${label(f)}: her own tile sown at the deployment (GUA-X stage 3 only)`);
const [a, b, wolf] = [h.unit(2), h.unit(3), h.unit(5)];
a.hp = a.s.maxHp * 0.5;
h.b.heal(u, a, 100);
const s = sownOf(u);
for (const [r, c] of [[9, 5], [10, 5], [9, 4], [9, 6]]) assert.ok(s.has(key(r, c)), `${label(f)}: ${r},${c}`);
assert.ok(!s.has(key(8, 5)), `${label(f)}: 8,5 is no walkable / deployable tile (A)`);
h.run(0.3);
assert.deepEqual(a.findBuff(FARM)?.mods, { hpRegen: t0.bb.hp_recovery_per_sec }, `${label(f)}: 生命回复速度`);
approx(a.findBuff('protect').data.value, t0.bb.damage_resistance, `${label(f)}: 庇护`);
approx(a.s.physTakenMul, 1 - t0.bb.damage_resistance, `${label(f)}: physical ×${1 - t0.bb.damage_resistance}`);
// a 无法被治疗 / 禁疗 unit still regenerates (an hpRegen attribute, no heal); a unit off the tiles gets nothing
h.b.addBuff(a, { key: 'test:noheal', flags: { noHeal: true, healFree: true } });
a.hp = a.s.maxHp * 0.5;
const hp0 = a.hp;
h.run(1);
approx(a.hp - hp0, t0.bb.hp_recovery_per_sec, `${label(f)}: +${t0.bb.hp_recovery_per_sec} HP in 1 s`, 0.05);
assert.equal(b.findBuff(FARM), null, `${label(f)}: off the tiles`);
// a summon (the 禁疗 wolf: no heal of hers reaches it) on a tile her heal of a neighbour sowed
h.b.relocate(b, 10, 7);
b.hp = b.s.maxHp * 0.5;
h.b.heal(u, b, 1);
assert.ok(sownOf(u).has(key(wolf.tileR, wolf.tileC)), `${label(f)}: the wolf's tile sown`);
h.run(0.3);
assert.ok(wolf.findBuff(FARM), `${label(f)}: a summon on a sown tile`);
// she leaves: every tile is cleared
h.b.retreat(u);
h.step();
assert.equal(a.findBuff(FARM), null, `${label(f)}: cleared at once`);
assert.equal(sownOf(u).size, 0, `${label(f)}: no sown tile left`);
h.run(0.5);
assert.equal(a.findBuff('protect'), null, `${label(f)}: 庇护 gone`);
done(h);
}
});
test('T2 天有四时: with her deployed, 3 professions on the field ⇒ every operator max HP +12 %, 3 operators of one profession ⇒ ASPD +12; 4 【岁】 in her squad ⇒ its operators ATK +12 % and 1 SP every 4 s, deployed or not', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const t1 = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
assert.deepEqual(t1, { max_hp: 0.12, attack_speed: 12, atk: 0.12, interval: 4, sp: 1 });
// 黍 (TANK) + 2 WARRIOR + 1 MEDIC: three professions; no three of one
{
const others = [{ uid: 2, chessId: 't_war', row: 12, col: 3 }, { uid: 3, chessId: 't_war2', row: 12, col: 5 }, { uid: 4, chessId: 't_med', row: 12, col: 7 }];
const { h, u } = field({ tier, elite, skill: 1, others });
h.run(0.3);
for (const x of [u, h.unit(2), h.unit(3), h.unit(4)]) {
assert.deepEqual(x.findBuff('talent:shu:seasons:hp')?.mods, { hpPct: t1.max_hp }, `T${tier}: max HP +12 % (${x.defId})`);
assert.equal(x.findBuff('talent:shu:seasons:aspd'), null, `T${tier}: no three of one`);
}
assert.equal(u.findBuff('talent:shu:sui'), null, `T${tier}: one 【岁】 only`);
h.b.retreat(u);
h.run(0.5);
assert.equal(h.unit(2).findBuff('talent:shu:seasons:hp'), null, `T${tier}: only while she is deployed`);
done(h);
}
// three WARRIOR: ASPD +12 (two professions only)
{
const others = [{ uid: 2, chessId: 't_war', row: 12, col: 3 }, { uid: 3, chessId: 't_war2', row: 12, col: 5 }, { uid: 4, chessId: 't_war3', row: 12, col: 7 }];
const { h, u } = field({ tier, elite, skill: 1, others });
h.run(0.3);
for (const x of [u, h.unit(2)]) {
assert.deepEqual(x.findBuff('talent:shu:seasons:aspd')?.mods, { aspd: t1.attack_speed }, `T${tier}: ASPD +12`);
assert.equal(x.findBuff('talent:shu:seasons:hp'), null, `T${tier}: two professions`);
}
done(h);
}
// four 【岁】: 黍, 重岳 (自选), 夕, 余 — and an operator whose SP comes only from gifts
{
const others = [{ uid: 2, diy: { slot: 6, charId: 'char_2024_chyue', skillIndex: 0 }, row: 12, col: 3 },
{ uid: 3, chessId: 'chess_char_5_12_a', row: 9, col: 3 }, { uid: 4, chessId: 'chess_char_6_03_a', row: 11, col: 3 },
{ uid: 5, chessId: 't_sp', row: 12, col: 8 }];
const { h, u } = field({ tier, elite, skill: 1, others });
h.step();
const sp = h.unit(5);
for (const x of [u, h.unit(2), h.unit(3), h.unit(4), sp]) assert.deepEqual(x.findBuff('talent:shu:sui')?.mods, { atkPct: t1.atk }, `T${tier}: ATK +12 % (${x.defId})`);
assert.equal(sp.skill.sp, 0);
h.run(3.9);
assert.equal(sp.skill.sp, 0, `T${tier}: nothing before 4 s`);
h.run(0.2);
assert.equal(sp.skill.sp, t1.sp, `T${tier}: +1 SP at 4 s`);
h.b.retreat(u);
h.run(4);
assert.equal(sp.skill.sp, 2 * t1.sp, `T${tier}: on without her deployed`);
done(h);
}
}
});
test('GUA-X “钦天司时”: her heals ×1.15 on a target at or below 50 % HP (stages 1 and 3), never above, never a regen tick; none without the module', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 't_a', row: 12, col: 9 }] });
const a = h.unit(2);
const on = elite && mod === GUAX;
if (on) assert.deepEqual(u.def.raw.trait.bb, { heal_scale: 1.15, hp_ratio: 0.5 }, label(f));
a.hp = a.s.maxHp * 0.5;
approx(h.b.heal(u, a, 1000), on ? 1150 : 1000, `${label(f)}: at 50 %`);
a.hp = a.s.maxHp * 0.51;
approx(h.b.heal(u, a, 1000), 1000, `${label(f)}: above 50 %`);
done(h);
}
});
+283
View File
@@ -0,0 +1,283 @@
// test/content/op_zumama.test.js — the 自选 operator kit of 森蚺 (char_416_zumama, 6★ 决战者; kit
// server/sim/content/kits/ops/op-zumama.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, HES-X
// “祖玛玛的工具箱”, HES-Y “小小的丑东西” or RA-A 森蚺特限证章 at stage 1 (tier 5) / 3 (tier 6). Every number is read back from
// data/backups.json (the form of that slot status); the fidelity checklist of kits/README.md item by item.
// Run: node --test test/content/op_zumama.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 ZUMAMA = 'char_416_zumama';
const FORMS = BACKUPS.units[ZUMAMA].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const HESX = 'uniequip_002_zumama', HESY = 'uniequip_003_zumama', RAA = 'uniequip_004_zumama';
const S1 = 'skchr_zumama_1', S2 = 'skchr_zumama_2', S3 = 'skchr_zumama_3';
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = {
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
enemy_tough: dummy('enemy_tough', { immunities: { stun: true } }),
enemy_frail: dummy('enemy_frail', { hp: 10 }),
};
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, HESX, HESY, RAA].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** A battle with 森蚺 as uid 1 at (row, col) facing RIGHT, plus `others`. */
function field({ tier = 5, elite = false, mod = null, skill = 1, row = 10, col = 5, others = [], seed = 5 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'heal', 'skillStart', 'skillEnd', 'statusApplied'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: ZUMAMA, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
/** An enemy she blocks: a still dummy put on her own tile (blocked on the next tick). */
function blockOne(h, u, key = 'enemy_dummy') {
const e = h.spawn(key, { pos: [u.tileR, u.tileC] });
assert.ok(h.runUntil(() => e.blockedBy === u, 1), 'blocked');
return e;
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
test('森蚺 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 1-1 range, blocks 1, melee ground-only, 萨尔贡, no 特质; triggers from the data', () => {
assert.equal(OPERATOR_KITS[ZUMAMA], KITS[ZUMAMA]);
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], [ZUMAMA, 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, 'melee', 'phys', false, 1.6], `${label(f)}: 决战者`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['sargonShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
// the 重装 exception (the owner's decision of 2026-10-05): S2 / S3 cast in range (DEFAULT), the official TAKE_DAMAGE kept
const sk = form.skills[skill];
if (skill === 0) assert.deepEqual([u.skill.kind, sk.skillType, u.skill.spCost], ['passive', 'PASSIVE', 0], `${label(f)}: S1 passive`);
else assert.deepEqual([u.skill.rule, sk.trigger.rule, sk.trigger.rawRule, sk.skillType], ['DEFAULT', 'DEFAULT', 'TAKE_DAMAGE', 'MANUAL'], `${label(f)}: S${skill + 1} trigger`);
done(h);
}
}
// zh_CN numbers: E2 Lv1 3440 / 835 / 492, E2 Lv60 4121 / 949 / 574; HES-X +80/+70 → +105/+85 ATK/DEF, HES-Y +70/+80 →
// +85/+110, RA-A +280/+80 → +380/+130 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], [3440, 835, 4121, 949]);
assert.deepEqual([modOf(5, HESX).attr, modOf(6, HESX).attr, modOf(5, HESY).attr, modOf(6, HESY).attr, modOf(5, RAA).attr, modOf(6, RAA).attr],
[{ atk: 80, def: 70 }, { atk: 105, def: 85 }, { atk: 70, def: 80 }, { atk: 85, def: 110 }, { maxHp: 280, atk: 80 }, { maxHp: 380, atk: 130 }]);
});
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(ZUMAMA));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(ZUMAMA), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: ZUMAMA, skillIndex: 2, uniEquipId: HESX } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: ZUMAMA, skillIndex: 2, uniEquipId: HESX } } });
});
test('S1 轻型挂斧 (PASSIVE): ATK / DEF +14 % / +18 % from every deployment, no SP', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const { h, u } = field({ tier, elite, skill: 0 });
assert.deepEqual([sk.bb.atk, sk.bb.def], elite ? [0.18, 0.18] : [0.14, 0.14], `T${tier}`);
assert.ok(u.skill.active, `T${tier}: on from the deployment`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
approx(u.s.def, u.base.def * (1 + sk.bb.def), `T${tier}: DEF`);
h.b.retreat(u);
h.b.redeploy(u);
h.step();
assert.ok(u.skill.active, `T${tier}: on again after a redeploy`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK again`);
done(h);
}
});
test('S2 震慑劈砍 (data DEFAULT: casts with the enemy she blocks): 16 / 17 s, ATK +100 % / +130 %, interval 1.6 + 0.4 s, every enemy she blocks stunned past 抵抗 while it lasts; a stun immunity holds; all gone after', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
const { h, u } = field({ tier, elite, skill: 1 });
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.spCost, sk.initSp], elite ? [17, 1.3, 0.4, 32, 12] : [16, 1, 0.4, 35, 11], `T${tier}`);
u.skill.gainSp(999, 'init');
h.run(2);
assert.equal(u.skill.activations, 0, `T${tier}: no enemy, no cast`);
const e = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC] });
h.b.applyStatus(e, 'resist', { duration: 999, value: 0.5 });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast once she blocks it`);
assert.equal(e.blockedBy, u);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.01);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
approx(u.s.interval, 2.0, `T${tier}: 1.6 + 0.4 s`);
h.step(2);
assert.ok(e.s.flags.stun, `T${tier}: the blocked enemy is stunned`);
const stuns = h.hooksOf('statusApplied').filter((c) => c.status === 'stun' && c.target === e && c.source === u);
assert.ok(stuns.length > 0 && stuns.every((c) => Math.abs(c.duration - 0.1) < 1e-9), `T${tier}: 抵抗 does not shorten it`);
h.run(5);
assert.ok(e.s.flags.stun && u.skill.active, `T${tier}: still stunned while blocked`);
// a new blockee mid-skill: the first one falls, a stun-immune one and a plain one come in turn
h.b.kill(e, u);
const tough = h.spawn('enemy_tough', { pos: [u.tileR, u.tileC] });
assert.ok(h.runUntil(() => tough.blockedBy === u, 1));
h.step(3);
assert.ok(!tough.s.flags.stun, `T${tier}: 晕眩 immunity`);
h.b.kill(tough, u);
const late = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC] });
assert.ok(h.runUntil(() => late.blockedBy === u, 1));
h.step(2);
assert.ok(late.s.flags.stun, `T${tier}: an enemy blocked mid-skill is stunned too`);
assert.ok(h.runUntil(() => !u.skill.active, sk.duration), `T${tier}: ends`);
h.run(0.15);
assert.ok(!late.s.flags.stun, `T${tier}: the stun ends with the skill`);
approx(u.s.interval, 1.6, `T${tier}: interval back`);
approx(u.s.atk, u.base.atk, `T${tier}: ATK back`);
// the stunned enemies fought nobody meanwhile: her normal attacks hit them (physical, ground)
assert.ok(atkHits(h, u).some((c) => c.target === late && c.type === 'phys'), `T${tier}: she keeps attacking`);
done(h);
}
});
test('S3 钢铁意志 (data DEFAULT): 31 / 32 s, ATK +140 % / +170 %, DEF +90 % / +120 %, block 1 + 2, 生命回复速度 3 % max HP / s (禁疗-proof); 5 s self-stun after, blocking nobody meanwhile', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, HESX]]) {
const sk = skillOf(tier, elite, S3);
const { h, u } = field({ tier, elite, mod, skill: 2 });
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.def, sk.bb.block_cnt, sk.bb.hp_recovery_per_sec_by_max_hp_ratio, sk.bb.stun], elite ? [32, 1.7, 1.2, 2, 0.03, 5] : [31, 1.4, 0.9, 2, 0.03, 5], `T${tier}`);
u.skill.gainSp(999, 'init');
const e = blockOne(h, u);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with the enemy she blocks`);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.05);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
approx(u.s.def, u.base.def * (1 + sk.bb.def), `T${tier}: DEF`);
assert.equal(u.s.blockCnt, 3, `T${tier}: block 1 + 2`);
approx(u.s.hpRegen, u.s.maxHp * 0.03, `T${tier}: 3 % max HP / s`);
// 生命回复速度, no heal: 禁疗 and 无法被治疗 do not stop it
h.b.addBuff(u, { key: 'test:healFree', flags: { healFree: true, noHeal: true } });
u.hp = u.s.maxHp * 0.5;
const hp0 = u.hp;
h.run(1);
approx(u.hp - hp0, u.s.maxHp * 0.03, `T${tier}: +3 % in 1 s under 禁疗`, 0.05);
h.b.removeBuff(u, 'test:healFree');
assert.ok(h.runUntil(() => !u.skill.active, sk.duration + 1), `T${tier}: ends`);
const st = h.hooksOf('statusApplied').find((c) => c.status === 'stun' && c.target === u);
assert.ok(st, `T${tier}: self-stun`);
approx(st.duration, sk.bb.stun, `T${tier}: ${sk.bb.stun} s`);
h.step(2);
assert.ok(u.s.flags.stun && u.blocking.length === 0 && e.blockedBy !== u, `T${tier}: stunned, blocking nobody`);
assert.equal(u.s.blockCnt, 1, `T${tier}: block back to 1`);
h.run(sk.bb.stun + 0.2);
assert.ok(!u.s.flags.stun, `T${tier}: the stun is over`);
done(h);
}
});
test('T1 勇冠三军: above half HP every hit ×1.15 as an 攻击力倍率 (×1.23 with HES-Y stage 3), at or below half 20 % 庇护 (28 %) on physical / arts damage only — checked every 0.1 s', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 1 });
const t0 = u.def.raw.talents.find((t) => t.index === 0).bb;
const y3 = elite && mod === HESY && tier === 6;
assert.deepEqual([t0.hp_ratio, t0.atk_scale, t0.damage_resistance], y3 ? [0.5, 1.23, 0.28] : [0.5, 1.15, 0.2], label(f));
const e = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC + 1] }); // in her 1-1, not blocked: no SP, no cast
h.run(0.2);
assert.equal(u.s.atkScaleMul, t0.atk_scale, `${label(f)}: above half`);
const n0 = atkHits(h, u).length;
assert.ok(h.runUntil(() => atkHits(h, u).length > n0, 3));
const hit = atkHits(h, u)[n0];
approx(hit.amount, u.s.atk * t0.atk_scale, `${label(f)}: ×${t0.atk_scale} on a 0-DEF target`);
// half HP: 庇护, no ×1.15
u.hp = u.s.maxHp * 0.5;
h.run(0.14);
assert.equal(u.s.atkScaleMul, 1, `${label(f)}: at half: no bonus`);
const pb = u.findBuff('protect');
assert.ok(pb, `${label(f)}: 庇护`);
approx(pb.data.value, t0.damage_resistance, `${label(f)}: ${t0.damage_resistance * 100} %`);
const hit1 = (amount, type) => { u.hp = u.s.maxHp * 0.5; return h.b.dealDamage(e, u, { amount, type }); };
approx(hit1(400, 'arts'), 400 * (1 - t0.damage_resistance), `${label(f)}: arts`);
approx(hit1(300, 'true'), 300, `${label(f)}: true untouched`);
approx(hit1(u.s.def + 400, 'phys'), 400 * (1 - t0.damage_resistance), `${label(f)}: physical`);
// back above half: the bonus within one check, the 庇护 gone
u.hp = u.s.maxHp * 0.9;
h.run(0.25);
assert.equal(u.s.atkScaleMul, t0.atk_scale, `${label(f)}: above half again`);
assert.equal(u.findBuff('protect'), null, `${label(f)}: no 庇护`);
done(h);
}
});
test('trait 决战者: no module ⇒ 阻回 (no SP of any kind, gifts too) unless she blocks; HES-X ×0.2 / HES-Y ×0.001 / RA-A ×0 SP recovery unblocked (gifts land); T2 愈战愈勇 +0.2 / s blocking (HES-X stage 3 +0.55; RA-A stays +0.2); HES-Y ATK / DEF +15 % blocking', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 1 });
const form = formOf(tier, elite);
const m = elite ? modOf(tier, mod) : null;
const ratio = m ? m.traitOverride.bb.sp_recover_ratio : null;
const t2 = elite && mod === HESX && tier === 6 ? 0.55 : 0.2;
assert.equal(ratio, { [HESX]: -0.8, [HESY]: -0.999, [RAA]: -1 }[mod] ?? null, label(f));
const init = skillOf(tier, elite, S2).initSp;
assert.ok(u.skill.sp >= init && u.skill.sp < init + 0.05, `${label(f)}: initSp at the deployment despite 阻回 (${u.skill.sp})`);
// unblocked
h.run(0.2);
const sp0 = u.skill.sp;
h.run(5);
const unblocked = u.skill.sp - sp0;
if (ratio == null) {
assert.ok(u.s.flags.noSp, `${label(f)}: 阻回`);
approx(unblocked, 0, `${label(f)}: no SP`);
assert.equal(u.skill.gainSp(3, 'talent'), 0, `${label(f)}: a gift refused`);
} else {
assert.ok(!u.s.flags.noSp, `${label(f)}: no 阻回 with a module`);
approx(u.s.spRecovery, 1 + ratio, `${label(f)}: ×${1 + ratio}`);
approx(unblocked, 5 * (1 + ratio), `${label(f)}: ${5 * (1 + ratio)} SP in 5 s`, 1e-3);
approx(u.skill.gainSp(3, 'talent'), 3, `${label(f)}: a gift lands`);
}
assert.equal(u.findBuff('trait:zumama:block'), null, `${label(f)}: nothing blocked`);
// blocking: full SP recovery + T2
u.skill.sp = 0;
blockOne(h, u);
h.step();
approx(u.s.spRecovery, 1 + t2, `${label(f)}: 1 + ${t2} / s blocking`);
const sp1 = u.skill.sp;
h.run(5);
approx(u.skill.sp - sp1, 5 * (1 + t2), `${label(f)}: SP while blocking`, 1e-3);
if (elite && mod === HESY) {
assert.deepEqual(u.findBuff('trait:zumama:block')?.mods, { atkPct: 0.15, defPct: 0.15 }, `${label(f)}: HES-Y`);
} else assert.equal(u.findBuff('trait:zumama:block'), null, `${label(f)}: no ATK / DEF trait`);
if (elite && mod === RAA && tier === 6) {
// the composed record carries the 生息演算-only 0.4 / s; the kit reads 愈战愈勇 from talentsBase
assert.equal(u.def.raw.talents.find((t) => t.index === 1).bb.sp_recovery_per_sec, 0.4, 'the sandbox part is in the record');
assert.equal(u.def.raw.talentsBase.find((t) => t.index === 1).bb.sp_recovery_per_sec, 0.2);
}
assert.equal(form.trait.desc, '只有阻挡敌人时才能够回复技力');
done(h);
}
});
test('RA-A 森蚺特限证章: only its attributes and its trait ratio apply here — no 生息演算 parts (S2 block stays 1, no 物理脆弱, no attraction)', () => {
for (const tier of [5, 6]) {
const { h, u } = field({ tier, elite: true, mod: RAA, skill: 1, row: 9, col: 5 });
const m = modOf(tier, RAA);
assert.match(m.traitOverride.moduleDesc, /生息演算/);
u.skill.gainSp(999, 'init');
const far = h.spawn('enemy_dummy', { pos: [12, 9] });
const e = blockOne(h, u);
assert.ok(h.runUntil(() => u.skill.active, 3));
assert.equal(u.s.blockCnt, 1, `T${tier}: no 技能期间阻挡数+2`);
approx(e.s.physTakenMul, 1, `T${tier}: no 物理脆弱`);
approx(far.x, 9, `T${tier}: nobody pulled`);
done(h);
}
});
+345 -98
View File
@@ -1974,8 +1974,8 @@
"snaps": "8eac4d32 fa5c2376 bc1e9184 84fb1db6 b4b3b807 cc6f6399 c665e4ff 261094cd 5d5c61fd 1bf27396 b526ffa6 e433a3a2 7d8bd521 d7eb687d 4d18ecc9 f640c5fd 0912a7d6 cc22df71 1a833e56 d877d28b 8a887ef8 8014c7dd"
},
"diy-049": {
"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=zumama/S1 5_diy2_a=zumama/S2 5_diy1_a=zumama/S3 5_diy2_b=zumama/S1 5_diy1_b=zumama/S2 5_diy2_b=zumama/S3 5_diy1_b=zumama/S1/002_zumama 5_diy2_b=zumama/S2/002_zumama 5_diy1_b=zumama/S3/002_zumama 5_diy2_b=zumama/S1/003_zumama 5_diy1_b=zumama/S2/003_zumama 5_diy2_b=zumama/S3/003_zumama",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m05",
"modeId": "mode_multi_normal",
@@ -1985,74 +1985,74 @@
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 2,
"killed": 5,
"total": 60,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":2,"total":60,"perfect":false,"leaks":58,"leaksUncounted":0,"coins":0,"dmg":267738,"bossDmg":0,"heal":59968,"deaths":6,"layerGains":{}}},
"players": {"p1":{"killed":5,"total":60,"perfect":false,"leaks":55,"leaksUncounted":0,"coins":0,"dmg":321707,"bossDmg":0,"heal":10404,"deaths":7,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,76,0,304,6384,0,22,1],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,75,0,652,8724,0,41,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,6,0,21282,21790,0,137,3],
["p1",4,"op","chess_char_5_diy2_b",2,"none",3334,1,8,2,22607,38177,0,115,4],
["p1",5,"op","chess_char_5_diy1_a",2,null,1271,1,10,0,2272,0,0,99,2],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,11,0,2916,0,0,108,3],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,1,0,0,0,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,3,0,97722,0,0,178,4],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,5,0,0,0,29529,69,4],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,7,0,0,0,30439,60,4],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,9,0,56880,0,0,144,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,12,0,63104,0,0,136,3]
["p1",1,"op","chess_char_5_diy1_a",0,null,545.31,1,75,0,15233,6335,0,74,0],
["p1",2,"op","chess_char_5_diy2_a",1,null,2006.6,1,76,0,24337,4873,0,75,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,3440,1,77,0,1179,6880,0,26,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,8,0,42923,4121,0,109,0],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,10,1,68502,4121,0,101,4],
["p1",6,"op","chess_char_5_diy2_b",2,"none",4121,1,11,4,128679,10404,10404,127,3],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_zumama",738.94,1,52,0,40854,7503,0,87,0],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_zumama",4121,1,1,0,0,0,0,0,0],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_zumama",4121,1,3,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_zumama",4121,1,5,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_zumama",4121,1,7,0,0,0,0,0,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_zumama",4121,1,9,0,0,0,0,0,0]
],
"enemies": {"enemy_1045_hammer_2":[12,0,11,37471,77249],"enemy_1061_zomshd":[48,2,45,37605,269489]},
"hooks": [1,76,6900,1205,1205,3051,3051,29902,8,64,33,29,0,37113,285,23,60,56,1,8,0,0,0],
"events": {"atk":1205,"deploy":16,"die":8,"dmg":3051,"heal":129,"leak":56,"skill":62,"spawn":72,"status":555},
"enemies": {"enemy_1045_hammer_2":[12,0,12,12431,26021],"enemy_1061_zomshd":[48,5,41,31806,382046]},
"hooks": [1,77,6900,817,817,2976,2976,36390,12,65,10,12,0,30526,3090,21,60,53,1,12,0,0,0],
"events": {"atk":817,"deploy":17,"die":12,"dmg":2976,"leak":53,"skill":19,"spawn":72,"status":22},
"fx": {},
"snaps": "c6bdc18a 6fb12fe1 c3c94d6f f42e9bba 6ea4c93a ff3506ac f551dbb2 248e7a29 e877982d 0b8ac60e d21ffc26 89eda7b5 7ef13ac0 2d2fce6f 32484e38 3407d0b5 1b59f452 55141007 fc87be97 6239dcab e11e96f9 250fe5b6 a75b766c"
"snaps": "e50d699b fff33f51 74e64c52 4d9ccf25 7eb25844 db86a212 b697c387 8355d887 bfb274bc 5bbe1567 8cdf0aa4 279f4fd2 b16b8a31 c4fdf474 26f84dd0 6b440128 6641ed13 8afb6d38 022dda2a 00107ee9 54ee2dbe 97022e88 79460fd6"
},
"diy-050": {
"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_b=zumama/S1/004_zumama 5_diy2_b=zumama/S2/004_zumama 5_diy1_b=zumama/S3/004_zumama 6_diy1_a=zumama/S1 6_diy2_a=zumama/S2 6_diy1_a=zumama/S3 6_diy2_b=zumama/S1 6_diy1_b=zumama/S2 6_diy2_b=zumama/S3 6_diy1_b=zumama/S1/002_zumama 6_diy2_b=zumama/S2/002_zumama 6_diy1_b=zumama/S3/002_zumama",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m06",
"modeId": "mode_multi_hard",
"round": 12,
"round": 13,
"seed": 574294847,
"waveId": "act1autochess_h05",
"time": 214.8,
"ticks": 6444,
"waveId": "act1autochess_h06",
"time": 209.53,
"ticks": 6286,
"reason": "cleared",
"killed": 0,
"total": 60,
"killed": 4,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 115,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":60,"perfect":false,"leaks":60,"leaksUncounted":0,"coins":0,"dmg":157435,"bossDmg":0,"heal":12039,"deaths":23,"layerGains":{}}},
"players": {"p1":{"killed":4,"total":105,"perfect":false,"leaks":101,"leaksUncounted":0,"coins":0,"dmg":75459,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,82,0,28,5208,0,2,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",3371,1,6,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,1529,1,9,0,20856,0,0,0,7],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,72,0,174,5712,0,6,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,67,0,13430,5276,0,29,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,78,0,61051,12366,0,48,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,85,0,698,3816,0,13,1],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,84,0,2755,4761,0,39,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,52,0,19051,2744,0,7,1],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,51,0,39393,3498,0,7,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,47,0,0,2448,1260,3,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,54,0,0,9324,10779,8,1]
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_004_zumama",0,0,84,1,15047,8802,0,15,0],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_004_zumama",4401,1,7,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_004_zumama",4401,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,109,0,4213,10320,0,14,0],
["p1",5,"op","chess_char_6_diy2_a",1,null,3440,1,123,0,234,3440,0,5,0],
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,73,1,12119,6880,0,17,0],
["p1",7,"op","chess_char_6_diy2_b",0,"none",0,0,68,0,5217,8242,0,14,0],
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,4,1,22260,4121,0,19,1],
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,6,1,10219,4121,0,13,0],
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_zumama",0,0,1,0,4028,4121,0,5,0],
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_zumama",0,0,3,0,1166,4121,0,2,0],
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_zumama",0,0,5,0,956,4121,0,6,0]
],
"enemies": {"enemy_1045_hammer_2":[12,0,12,15694,22503],"enemy_1061_zomshd":[48,0,48,39458,134932]},
"hooks": [1,85,6444,245,245,412,396,10112,23,83,15,15,0,16629,0,28,60,60,1,26,0,0,16],
"events": {"atk":263,"deploy":25,"die":23,"dmg":396,"fx":86,"heal":23,"leak":60,"skill":30,"spawn":72,"status":14},
"fx": {"aoe":5,"dodge":16,"dp":7,"slash":56,"spGain":2},
"snaps": "580b238a aa91295b 3af047b3 a833e612 f6a0b595 1eba1563 0c047aab 7838a66c 34fc5adc 627a4814 4d7c5d84 41a4b79a 721f8563 f6d5a347 76cda6c2 74e73fdb 220b1dd6 2f7b90e4 d3a2ce26 6a84c6a3 1aae6574 0e8ef572"
"enemies": {"enemy_1165_duhond":[51,4,47,1988,40132],"enemy_1174_duholy_2":[24,0,24,18921,3266],"enemy_1175_dushdo_2":[30,0,30,37379,32062]},
"hooks": [1,123,6286,212,212,212,212,0,19,120,1,9,0,4860,510,19,105,101,1,19,0,0,0],
"events": {"atk":212,"deploy":18,"die":19,"dmg":212,"leak":101,"skill":2,"spawn":117,"status":163},
"fx": {},
"snaps": "4eb4081c f27d705e 12a5e103 ebd0ced8 c0af4450 f8987622 9804172d 98431192 d5001675 04f3813d c7edb47e 7cb6f5bb e1d82438 31f8dc00 b31c38fe 9e6c2ead 988fa49c 6362d373 55cdf373 91661d82 7d853e19"
},
"diy-051": {
"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=judge/S1 5_diy2_a=judge/S2 5_diy1_a=judge/S3 5_diy2_b=judge/S1 5_diy1_b=judge/S2 5_diy2_b=judge/S3 6_diy1_b=zumama/S1/003_zumama 6_diy2_b=zumama/S2/003_zumama 6_diy1_b=zumama/S3/003_zumama 6_diy2_b=zumama/S1/004_zumama 6_diy1_b=zumama/S2/004_zumama 6_diy2_b=zumama/S3/004_zumama",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
@@ -2060,47 +2060,47 @@
"round": 13,
"seed": 3864737019,
"waveId": "act1autochess_h06",
"time": 204.8,
"ticks": 6144,
"time": 202.97,
"ticks": 6089,
"reason": "cleared",
"killed": 197,
"total": 290,
"killed": 333,
"total": 345,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 282,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":197,"total":290,"perfect":false,"leaks":93,"leaksUncounted":0,"coins":0,"dmg":444885,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"players": {"p1":{"killed":333,"total":345,"perfect":false,"leaks":12,"leaksUncounted":0,"coins":0,"dmg":588871,"bossDmg":0,"heal":2878,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,4,7,21881,2604,0,65,1],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,7,11,35637,3431,0,87,2],
["p1",3,"op","chess_char_5_diy1_a",0,null,1560.61,1,279,11,28493,2169,0,91,2],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",1890.36,1,277,51,98505,2858,0,110,3],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,241,13,39114,3058,0,54,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,163,43,73993,3808,0,55,4],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,285,5,26351,8078,0,87,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,3,7,19,0,0,16,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,2638,1,5,10,60,0,0,60,3],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",2339.65,1,8,13,64107,1191,0,82,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,6,10,18397,0,0,78,2],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,9,16,38328,0,0,93,2],
["p1",1,"op","chess_char_5_diy1_a",0,null,2960,1,4,10,79,0,0,64,20],
["p1",2,"op","chess_char_5_diy2_a",1,null,2960,1,6,20,138,0,0,40,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,2960,1,8,22,772,0,0,90,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",3686,1,10,30,16779,0,0,108,37],
["p1",5,"op","chess_char_5_diy1_b",1,"none",3686,1,12,133,85616,0,0,71,4],
["p1",6,"op","chess_char_5_diy2_b",2,"none",3686,1,13,46,256155,0,0,98,4],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_003_zumama",1825.88,1,14,25,139596,2295,0,124,0],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_003_zumama",4121,1,3,2,18,0,0,18,0],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_003_zumama",4121,1,5,7,33,0,0,33,0],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_004_zumama",4501,1,7,15,48,0,0,48,0],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_004_zumama",4161.84,1,9,10,8735,339,0,55,0],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_004_zumama",4501,1,11,13,80901,2878,2878,76,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_1195_sfyin": [39,30,9,4143,113683],
"enemy_1196_msfyin": [60,32,28,0,65],
"enemy_1197_sfshu": [48,38,10,10130,184299],
"enemy_1198_msfshu": [114,78,36,0,243],
"enemy_1199_sfjin": [18,11,7,12925,146349],
"enemy_1200_msfjin": [11,8,3,0,246]
"enemy_1195_sfyin": [39,39,0,979,145567],
"enemy_1196_msfyin": [78,77,1,0,155],
"enemy_1197_sfshu": [48,48,0,1141,223949],
"enemy_1198_msfshu": [144,143,1,0,431],
"enemy_1199_sfjin": [18,18,0,3393,218350],
"enemy_1200_msfjin": [18,8,10,0,419]
},
"hooks": [1,314,6144,1470,1470,1969,2082,0,212,305,25,30,0,33234,6218,91,290,93,1,212,0,0,11],
"events": {"atk":1512,"deploy":22,"die":212,"dmg":2076,"fx":273,"leak":93,"skill":48,"spawn":304,"status":488},
"fx": {"aoe":56,"displace":1,"dodge":22,"dp":63,"slash":52,"summon":79},
"snaps": "cf051c06 587dcba3 94ed6adb bba4fd81 815661ad cbf17744 a8d0c19b 32b88916 1e8bde83 51a1d669 b023b185 df725cf4 bf4681aa 2c372e04 1a001766 3d81d3d6 7f8effe5 11826d80 b4fb05d0 0eed6d6c 65f6503d"
"hooks": [1,359,6089,1244,1244,2075,2075,645,333,345,70,68,0,32323,4,106,345,12,1,333,0,0,0],
"events": {"atk":1244,"deploy":12,"die":333,"dmg":1817,"fx":658,"leak":12,"skill":138,"spawn":359,"status":14},
"fx": {"aoe":136,"crit":5,"shield":154,"summon":105,"thorns":258},
"snaps": "480b65b5 19a0bccf 9c4c613e ada5b78a bdff8e84 b8d469f6 f336d7de 09ddcbef a4e6acba fbbbb637 0b162d9c 9e4a3728 1d7bc70c bd0bb3d2 112713bd 932890d6 70405bff ca609a0e 121cd1f9 bc0b661a 25692e57"
},
"diy-052": {
"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_b=judge/S1/002_judge 5_diy2_b=judge/S2/002_judge 5_diy1_b=judge/S3/002_judge 5_diy2_b=judge/S1/003_judge 5_diy1_b=judge/S2/003_judge 5_diy2_b=judge/S3/003_judge 6_diy1_a=judge/S1 6_diy2_a=judge/S2 6_diy1_a=judge/S3 6_diy2_b=judge/S1 6_diy1_b=judge/S2 6_diy2_b=judge/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
@@ -2108,35 +2108,282 @@
"round": 13,
"seed": 1210502478,
"waveId": "act1autochess_h06",
"time": 218.57,
"ticks": 6557,
"time": 228.33,
"ticks": 6850,
"reason": "cleared",
"killed": 0,
"killed": 17,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 81,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":76446,"bossDmg":0,"heal":2762,"deaths":33,"layerGains":{}}},
"players": {"p1":{"killed":17,"total":123,"perfect":false,"leaks":106,"leaksUncounted":0,"coins":0,"dmg":814321,"bossDmg":0,"heal":0,"deaths":16,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,6,0,2101,1865,0,8,0],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,9,0,4660,2494,0,8,0],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,157,0,13058,11540,0,21,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,156,0,24836,14840,0,17,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,151,0,3559,2544,0,18,0],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,3,0,11692,1643,0,23,0],
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,4,0,9700,1372,0,5,1],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,11,0,1022,1819,0,1,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,86,0,0,4120,2092,4,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,74,0,0,4018,670,2,0],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,1,0,1487,1160,0,3,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,5,0,4330,1450,0,7,1]
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_judge",0,0,111,0,12835,7672,0,9,2],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_judge",0,0,130,1,126079,7672,0,7,1],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_judge",3836,1,141,0,5437,3836,0,5,0],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_judge",0,0,4,0,8022,3686,0,5,2],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_judge",0,0,9,0,98776,3686,0,1,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_judge",0,0,5,1,47609,3686,0,13,1],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,82,0,17759,5920,0,16,4],
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,1,0,6789,2960,0,0,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,2,0,656,2960,0,1,0],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,6,0,8755,3686,0,5,2],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,74,0,8632,7372,0,7,0],
["p1",12,"op","chess_char_6_diy2_b",2,"none",3686,1,72,15,472972,3686,0,78,3]
],
"enemies": {"enemy_1019_jshoot_2":[57,0,57,29550,4113],"enemy_1299_ymkilr_2":[27,0,27,14576,26756],"enemy_1422_lrsldr_2":[39,0,39,4739,45577]},
"hooks": [1,157,6557,277,277,283,291,7,33,156,4,24,0,6272,192,29,123,123,1,33,0,0,0],
"events": {"atk":278,"deploy":34,"die":33,"dmg":269,"fx":19,"heal":7,"leak":123,"skill":8,"spawn":135,"status":92},
"fx": {"aoe":2,"displace":15,"spGain":2},
"snaps": "21be44ba 23e43266 29788d55 2f1037e9 363ad836 051606c5 26acdfeb 8dc54607 7a7c6a86 7ebe59b3 d1661fdb 203d434c 2e82423b f7e9ffc1 7a1afa0e 8998df31 0ca38cb6 f3ac5677 87bcc7cf 0f5f3879 0e082cac 62241cef"
"enemies": {"enemy_1019_jshoot_2":[57,15,42,4040,485283],"enemy_1299_ymkilr_2":[27,0,27,48725,117616],"enemy_1422_lrsldr_2":[39,2,37,4057,211421]},
"hooks": [1,141,6850,786,786,1585,1585,0,33,139,17,17,0,5543,4,62,123,106,1,33,0,0,0],
"events": {"atk":786,"deploy":18,"die":33,"dmg":1150,"fx":534,"leak":106,"skill":34,"spawn":135,"status":44},
"fx": {"aoe":39,"crit":4,"shield":39,"thorns":452},
"snaps": "85336f70 15ebb4f8 e27a3972 5b05e977 964e71a7 17c8c498 27fe59a4 3c4e9edb 46175bdc cbb0d18a 3a0ff874 82b99fce 6171a56d f8de78dd aabfedb5 ce9c3684 d28551db 22d6ef23 fe161e7f 8432bd16 ac3231ca 78b66a51 0bf9b3c1"
},
"diy-053": {
"about": "6_diy1_b=judge/S1/002_judge 6_diy2_b=judge/S2/002_judge 6_diy1_b=judge/S3/002_judge 6_diy2_b=judge/S1/003_judge 6_diy1_b=judge/S2/003_judge 6_diy2_b=judge/S3/003_judge 5_diy1_a=shu/S1 5_diy2_a=shu/S2 5_diy1_a=shu/S3 5_diy2_b=shu/S1 5_diy1_b=shu/S2 5_diy2_b=shu/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_multi_funny",
"round": 13,
"seed": 2850577873,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 99,
"total": 114,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":99,"total":114,"perfect":false,"leaks":15,"leaksUncounted":0,"coins":0,"dmg":839661,"bossDmg":0,"heal":16410,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_judge",3986,1,3,0,0,0,0,0,0],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_judge",3986,1,4,0,0,0,0,0,0],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_judge",3986,1,5,0,0,0,0,0,0],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_judge",3686,1,7,2,27858,0,0,17,7],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_judge",3686,1,11,33,289297,0,0,62,4],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_judge",3686,1,13,55,458054,0,0,121,4],
["p1",7,"op","chess_char_5_diy1_a",0,null,2301.12,1,14,1,9717,4624,4612,207,1],
["p1",8,"op","chess_char_5_diy2_a",1,null,2305.79,1,8,0,1488,4204,3956,91,4],
["p1",9,"op","chess_char_5_diy1_a",2,null,2313,1,12,1,12581,5038,4654,222,3],
["p1",10,"op","chess_char_5_diy2_b",0,"none",2910,1,9,3,16757,1484,1269,93,0],
["p1",11,"op","chess_char_5_diy1_b",1,"none",2903.49,1,10,0,702,1086,1199,39,2],
["p1",12,"op","chess_char_5_diy2_b",2,"none",2910,1,6,4,23207,0,720,99,4],
["-","~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_1381_winman":[27,24,0,4454,133632],"enemy_1381_winman_2":[48,44,0,5321,330476],"enemy_1387_winshd":[39,31,0,6660,375553]},
"hooks": [1,128,6900,2354,2354,2853,2853,6036,99,99,29,28,0,19829,2,113,114,0,1,99,0,0,0],
"events": {"atk":2360,"deploy":12,"die":99,"dmg":2504,"fx":538,"heal":128,"skill":57,"spawn":128,"status":9},
"fx": {"aoe":80,"crit":2,"healField":14,"shield":93,"thorns":349},
"snaps": "a1804ed0 6e1f8881 cb71b3fe 7183a61b de629b62 8eb2f85b 02beb5c8 2f6f897d 8a723304 13b316ca 3dad4830 f0a40c6c b9448612 1edb97a9 97a1869d 364d6c22 d384e689 c20a1b23 9f9f6541 79a07380 41ff316d df034031 a4a58a77"
},
"diy-054": {
"about": "5_diy1_b=shu/S1/002_shu 5_diy2_b=shu/S2/002_shu 5_diy1_b=shu/S3/002_shu 6_diy1_a=shu/S1 6_diy2_a=shu/S2 6_diy1_a=shu/S3 6_diy2_b=shu/S1 6_diy1_b=shu/S2 6_diy2_b=shu/S3 6_diy1_b=shu/S1/002_shu 6_diy2_b=shu/S2/002_shu 6_diy1_b=shu/S3/002_shu",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m03",
"modeId": "mode_single_hard",
"round": 13,
"seed": 196461732,
"waveId": "act1autochess_h06",
"time": 229.47,
"ticks": 6884,
"reason": "cleared",
"killed": 0,
"total": 66,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":66,"perfect":false,"leaks":66,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":66233,"deaths":18,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_shu",3070,1,91,0,0,3844,774,0,0],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_shu",3070,1,90,0,0,10337,3371,0,0],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_shu",3070,1,10,0,0,0,22156,39,3],
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,60,0,0,9980,674,0,0],
["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,57,0,0,24040,3795,0,0],
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,67,0,0,7853,6318,11,1],
["p1",7,"op","chess_char_6_diy2_b",0,"none",0,0,64,0,0,9827,1323,0,0],
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,84,0,0,8730,0,0,0],
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,5,0,0,3344,7037,15,1],
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_shu",0,0,86,0,0,22809,3390,0,0],
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_shu",3180,1,9,0,0,0,0,0,0],
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_shu",0,0,62,0,0,15665,17395,24,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_1005_yokai_2":[12,0,12,2003,0],"enemy_1041_lazerd_2":[24,0,24,114427,0],"enemy_1042_frostd":[30,0,30,0,0]},
"hooks": [1,91,6884,484,484,390,390,7117,18,84,7,7,0,10950,0,0,66,66,1,18,0,0,0],
"events": {"atk":484,"deploy":22,"die":18,"dmg":390,"fx":20,"heal":88,"leak":66,"skill":14,"spawn":81,"status":48},
"fx": {"healField":20},
"snaps": "f04fb9cf 0516108d 04b708fd 76eed263 ac69ca8f 840db29d e2c129db 23c826b8 4dfaf782 b14af258 5fc2160c 2c69b3bd a1718c78 1dd0b441 129f07c0 6526dab9 f46d2b09 b032c87a 6fff1120 371ca359 e7afd5c6 ff07b06c cb476ff1"
},
"diy-055": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_multi_normal",
"round": 12,
"seed": 1597707536,
"waveId": "act1autochess_h05",
"time": 226.43,
"ticks": 6793,
"reason": "cleared",
"killed": 8,
"total": 117,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":8,"total":117,"perfect":false,"leaks":109,"leaksUncounted":0,"coins":0,"dmg":52598,"bossDmg":0,"heal":8900,"deaths":25,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,117,0,0,6078,0,0,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,111,0,0,7823,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,144,0,0,5128,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",208.49,1,145,0,0,10902,0,0,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,132,0,18064,2542,0,48,1],
["p1",6,"op","chess_char_5_diy2_b",2,"none",868.62,1,146,5,21138,3661,0,44,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,86,1,660,2480,0,2,0],
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,79,1,4810,2924,0,9,0],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,5,0,0,1218,5678,12,1],
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,92,0,0,4841,3223,7,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,142,0,3950,2796,0,11,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,135,1,3976,4845,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_1005_yokai":[51,8,43,0,18454],"enemy_1005_yokai_2":[54,0,54,21039,2515],"enemy_1005_yokai_3":[12,0,12,34200,31629]},
"hooks": [1,146,6793,492,492,425,425,19,33,142,4,4,0,11479,18,0,117,109,1,33,0,0,0],
"events": {"atk":492,"deploy":27,"die":33,"dmg":425,"heal":19,"leak":109,"skill":8,"spawn":131,"status":28},
"fx": {},
"snaps": "e90d8ac5 3e7826d8 2ea79392 0d0a30f6 55810e9b a62c5a87 b1c5cfca 6959234f 84a973da 3386636e fa19929f 31ac9ec4 cc1b23bc 0ce6593e 1aa312f9 8de415cc 7670e151 f5f13c6f 12c4e5a4 75fb7a93 385d158a f1ce79bb 455d3b6d"
},
"diy-056": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_multi_hard",
"round": 12,
"seed": 3238296381,
"waveId": "act1autochess_h05",
"time": 208.03,
"ticks": 6241,
"reason": "cleared",
"killed": 3,
"total": 75,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 196,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":3,"total":75,"perfect":false,"leaks":72,"leaksUncounted":0,"coins":0,"dmg":162615,"bossDmg":0,"heal":9891,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,7,0,249,2808,0,3,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,9,1,600,3371,0,5,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,97,1,12118,4587,0,5,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,101,0,24851,5712,0,11,1],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,68,0,6186,5276,0,8,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,8,0,35092,8965,0,13,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,75,0,17393,2748,0,33,1],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,84,1,48820,3924,0,40,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,61,0,8325,3344,0,4,0],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,55,0,8980,3498,0,4,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,93,0,0,3198,8062,15,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,102,0,0,4657,1829,3,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_1267_nhpbr_2":[36,2,34,5542,68908],"enemy_1270_nhstlk_2":[27,1,26,12814,85923],"enemy_1272_nhtank_2":[12,0,12,33732,7784]},
"hooks": [1,102,6241,222,222,529,526,32,26,98,6,6,0,8985,0,21,75,72,1,56,0,0,3],
"events": {"atk":261,"deploy":24,"die":26,"dmg":526,"fx":42,"heal":32,"leak":72,"skill":12,"spawn":90,"status":32},
"fx": {"dodge":3,"dp":3,"slash":16,"spGain":2,"zone":18},
"snaps": "4a66aa50 cd957f8d 29040588 85dbfdfb 24a06993 403a5cd0 af8c7db4 ece6f6e6 e208e92d b00a9483 cdbb6067 b86efe8c 4d65b9e6 9e9b8ccc 4dd884e0 de6f0bb5 1ee54cb6 187ba789 edc6ec98 27566532 7101f464"
},
"diy-057": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_single_normal",
"round": 13,
"seed": 583790378,
"waveId": "act1autochess_h06",
"time": 229.57,
"ticks": 6887,
"reason": "cleared",
"killed": 25,
"total": 147,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 349,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":25,"total":147,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":447188,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,144,0,5014,5208,0,37,1],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,1,0,7239,3431,0,80,2],
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,143,0,12621,3730,0,59,1],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,139,5,129318,4748,0,83,2],
["p1",5,"op","chess_char_6_diy1_a",2,null,1529,1,176,2,19391,3058,0,28,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1904,1,174,8,121508,3808,0,33,4],
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,165,0,5198,6924,0,54,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,171,0,7386,4344,0,52,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,2638,1,177,2,17400,2638,0,21,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,146,6,70277,7062,0,82,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,6,0,21302,0,0,139,3],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,8,2,30532,0,0,150,3]
],
"enemies": {"enemy_1166_dusbr_2":[39,10,29,2081,102398],"enemy_1169_duphlx_2":[57,9,48,10670,156040],"enemy_1170_dushld_2":[51,6,45,32200,188750]},
"hooks": [1,177,6887,1087,1087,1627,1734,0,48,170,21,28,0,26130,14208,52,147,122,1,48,0,0,14],
"events": {"atk":1145,"deploy":30,"die":48,"dmg":1705,"fx":155,"leak":122,"skill":40,"spawn":159,"status":1199},
"fx": {"aoe":51,"displace":8,"dodge":32,"dp":16,"slash":48},
"snaps": "3c92533d c853aee7 5b1063fd 5fc57b7e 4a05e8e6 38a71bec 3070ca8c d432fa88 49683316 30a3854a 9104d2b4 923ae15a 22aa5459 cdd051aa 9f802034 82bafa84 ccf4b89d e3e17beb 6eb62b58 fe7abec6 989ccd10 d648552b 98e9bf71"
},
"diy-058": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_multi_abyss",
"round": 13,
"seed": 2224511831,
"waveId": "act1autochess_h06",
"time": 208.07,
"ticks": 6242,
"reason": "cleared",
"killed": 2,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 148,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":2,"total":105,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":0,"dmg":240102,"bossDmg":0,"heal":14281,"deaths":30,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,5,0,14676,1865,0,12,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,7,1,65680,3799,0,21,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,140,0,19129,12041,0,30,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,139,0,38020,15591,0,35,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,131,0,5981,1272,0,26,0],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,12,0,8584,1643,0,26,0],
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,78,0,5748,4303,0,7,0],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,4,1,75199,1819,0,10,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,6,0,0,2056,5589,7,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,9,0,0,5567,8692,11,1],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,13,0,3038,1160,0,13,0],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,73,0,4048,5494,0,11,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_1229_darmy":[27,1,26,5045,88057],"enemy_1229_darmy_2":[57,1,56,19387,116235],"enemy_1275_dwlock_2":[21,0,21,32176,35810]},
"hooks": [1,140,6242,294,294,501,589,31,32,135,6,24,0,7426,388,24,105,103,1,32,0,4,3],
"events": {"atk":328,"deploy":32,"die":32,"dmg":541,"fx":78,"heal":27,"leak":103,"skill":12,"spawn":120,"status":178},
"fx": {"aoe":8,"beam":24,"burst":4,"displace":25,"dodge":5,"spGain":12},
"snaps": "c9dff647 505536a6 046d3d99 29e1ce4f 95f692ed cb471e6e 3e929e64 72083a81 77cee0d9 d920f33c d26b1231 10d91924 2bc3222a 3b3d8eb1 a41709a6 a48f0d23 774d80e5 2256086c dbd65fc9 29f0c70c fa23ead3"
}
}
}