feedback5: merge O9 — 自选 kits for 帕拉斯, 止颂 and 贝洛内 (with his 牵绊); golden: only diy moved

This commit is contained in:
sganggs committed 2026-10-06 01:33:46 +08:00
commit a2d2f6a12f
8 files changed
+2056 -96

No files matched your search

+377
View File
@@ -0,0 +1,377 @@
// test/content/op_demetr.test.js — the 自选 operator kit of 贝洛内 (char_4037_demetr, 6★ 斗士; kit
// server/sim/content/kits/ops/op-demetr.js) and his summon 牵绊 (token_10065_demetr_dmtpos), 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 FGT-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_demetr.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 { BOND, MEANS_KEY, MEANS_SLUG_KEY, STREET_KEY, UNDEAD_KEY, RETURN_UNDYING } from '../../server/sim/content/kits/ops/op-demetr.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 DEMETR = 'char_4037_demetr';
const FORMS = BACKUPS.units[DEMETR].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const FGTY = 'uniequip_002_demetr';
const S1 = 'skchr_demetr_1', S2 = 'skchr_demetr_2', S3 = 'skchr_demetr_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 talentOf = (u, i) => u.def.raw.talents.find((t) => t.index === i)?.bb ?? {};
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = {
enemy_dummy: dummy('enemy_dummy'), enemy_armor: dummy('enemy_armor', { def: 1000 }), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
enemy_elite: dummy('enemy_elite', { rank: 'ELITE' }), enemy_hit: dummy('enemy_hit', { atk: 3000, bat: 1 }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, FGTY].map((m) => [t, true, m]))];
/** 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, rows = null } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 600, autoFinish: false, seed, ...(rows ? { flat: { rows } } : {}),
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'deploy', 'death'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: DEMETR, 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);
const moves = (h, u) => h.hooksOf('deploy').filter((c) => c.unit === u && c.move);
const bondOf = (h) => h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === BOND && a.alive) ?? null;
const noSp = (h, u) => h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } }); // 阻回: the MANUAL skill stays off
const meansOf = (e) => e.findBuff(MEANS_KEY)?.data?.stacks ?? 0;
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
test('贝洛内 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, blocks 1, melee ground-only, 0.78 s, 叙拉古, no 特质', () => {
assert.equal(OPERATOR_KITS[DEMETR], KITS[DEMETR]);
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], [DEMETR, 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.base.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.sub, u.base.bat, u.dmgType], [1, 'melee', false, 'fighter', 0.78, 'phys'], `${label(f)}: 斗士`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.raw.nationId], [['siracusaShip'], [], 'siracusa'], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
done(h);
}
}
// E2 Lv1 2152 / 490 / 298, E2 Lv60 2466 / 566 / 336; FGT-Y +140 / +50 → +230 / +80
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], [2152, 490, 2466, 566]);
assert.deepEqual([modOf(5, FGTY).attr, modOf(6, FGTY).attr], [{ maxHp: 140, atk: 50 }, { maxHp: 230, atk: 80 }]);
});
test('a 自选 pick: 贝洛内 is offered at tiers 5 and 6 and a roster with him passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(DEMETR));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(DEMETR), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: DEMETR, skillIndex: 2, uniEquipId: FGTY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: DEMETR, skillIndex: 2, uniEquipId: FGTY } } });
});
test('FGT-Y “实用的工具”: ASPD +10 while his HP is above 50 % (stages 1 and 3); none without it', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0 });
const on = elite && mod === FGTY;
if (on) assert.deepEqual(u.def.raw.trait.bb, { attack_speed: 10, hp_ratio: 0.5 }, label(f));
h.step();
assert.equal(u.s.aspd, 100 + (on ? 10 : 0), `${label(f)}: full HP`);
u.hp = u.s.maxHp * 0.5;
h.step();
assert.equal(u.s.aspd, 100, `${label(f)}: at 50 %`);
u.hp = u.s.maxHp * 0.51;
h.step();
assert.equal(u.s.aspd, 100 + (on ? 10 : 0), `${label(f)}: above again`);
done(h);
}
});
test('T1 家族手段: each attack hit puts one more 【手段】 stack on its target before its damage (DEF ×(1 − 7 % × n), FGT-Y stage 3: 8 %; at most 5) for 10 s; and his damage ×1 → ×1.28 (×1.42) linearly as the target falls from 100 % to 20 % HP', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2 });
noSp(h, u);
const t = talentOf(u, 0);
const y3 = elite && mod === FGTY && tier === 6;
assert.deepEqual([t['attack@def'], t['attack@limited_stack_cnt'], t['attack@s2_limited_stack_cnt'], t['attack@def_dec_duration'], t.min_hp_ratio, t.max_hp_ratio, t.max_add_on_scale],
[y3 ? -0.08 : -0.07, 5, 8, 10, 1, 0.2, y3 ? 0.42 : 0.28], `${label(f)}: the talent of the form`);
const pct = -t['attack@def'];
const e = h.spawn('enemy_armor', { pos: [10, 6] });
const n0 = atkHits(h, u).length;
h.run(8);
const hits = atkHits(h, u).slice(n0);
assert.ok(hits.length >= 7, `${label(f)}: ${hits.length} attacks`);
hits.forEach((c, i) => {
const k = Math.min(5, i + 1);
approx(c.amount, Math.max(u.s.atk - 1000 * (1 - pct * k), u.s.atk * 0.05), `${label(f)}: attack ${i + 1} at ${k} stack(s)`);
});
assert.equal(meansOf(e), 5, `${label(f)}: 5 at most`);
approx(e.s.def, 1000 * (1 - pct * 5), `${label(f)}: DEF ×${1 - pct * 5} (final multiplier)`);
// the HP bonus: 60 % HP ⇒ halfway; 20 % HP and below ⇒ the most
for (const [ratio, frac] of [[0.6, 0.5], [0.2, 1], [0.05, 1]]) {
e.hp = e.s.maxHp * ratio;
const m0 = atkHits(h, u).length;
h.runUntil(() => atkHits(h, u).length > m0, 3);
const c = atkHits(h, u)[m0];
approx(c.amount, Math.max(u.s.atk - e.s.def, u.s.atk * 0.05) * (1 + t.max_add_on_scale * frac), `${label(f)}: at ${ratio * 100} % HP ×${1 + t.max_add_on_scale * frac}`);
}
// 10 s after the last hit it is gone
h.b.retreat(u);
h.run(9.5);
assert.equal(meansOf(e), 5, `${label(f)}: still there after 9.5 s`);
h.run(0.6);
assert.equal(e.findBuff(MEANS_KEY), null, `${label(f)}: gone 10 s after his last hit`);
approx(e.s.def, 1000, `${label(f)}: DEF back`);
done(h);
}
});
test('T2 街头直觉: 80 % physical and arts dodge from every deployment, −2 % each second for 20 s, then 40 % for good', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0 });
const t = talentOf(u, 1);
assert.deepEqual([t.init_prob, t.dec_prob, t.trig_cnt], [0.8, 0.02, 20], label(f));
approx(u.s.dodgePhys, 0.8, `${label(f)}: at the deployment`);
assert.deepEqual(u.findBuff(STREET_KEY)?.mods, { dodgePhys: 0.8, dodgeArts: 0.8 }, `${label(f)}: one buff, both types`);
approx(u.s.dodgeArts, 0.8, `${label(f)}: arts too`);
h.run(1.02);
approx(u.s.dodgePhys, 0.78, `${label(f)}: after 1 s`);
h.run(9);
approx(u.s.dodgePhys, 0.6, `${label(f)}: after 10 s`);
h.run(10);
approx(u.s.dodgePhys, 0.4, `${label(f)}: after 20 s`);
h.run(30);
approx(u.s.dodgeArts, 0.4, `${label(f)}: stays 40 %`);
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
let dodged = 0;
for (let i = 0; i < 400; i++) { u.hp = u.s.maxHp; if (h.b.dealDamage(e, u, { amount: 10, type: i % 2 ? 'phys' : 'arts' }) === 0) dodged++; }
assert.ok(Math.abs(dodged / 400 - 0.4) < 0.07, `${label(f)}: ${dodged} / 400 dodged`);
h.b.retreat(u);
h.b.redeploy(u);
h.step();
approx(u.s.dodgePhys, 0.8, `${label(f)}: a new deployment starts again`);
done(h);
}
});
test('S1 家主的余裕 (AUTO, attack SP 4, data DEFAULT): the next attack strikes twice for 180 % / 210 % ATK — two 【手段】 stacks', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, FGTY]]) {
const sk = skillOf(tier, elite, S1);
const { h, u } = field({ tier, elite, mod, skill: 0 });
assert.deepEqual([u.skill.rule, u.skill.spType, u.skill.spCost, sk.skillType, sk.bb.atk_scale], ['DEFAULT', 'attack', 4, 'AUTO', elite ? 2.1 : 1.8], `T${tier}`);
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => atkHits(h, u).some((c) => c.dmg.isSkill), 20), `T${tier}: cast`);
const sk1 = atkHits(h, u).filter((c) => c.dmg.isSkill);
assert.equal(sk1.length, 2, `T${tier}: two strikes`);
assert.equal(atkHits(h, u).filter((c) => !c.dmg.isSkill).length, 4, `T${tier}: on the 5th attack`);
// (×1.0000x: the target's HP is not quite full any more — 家族手段's HP bonus)
for (const c of sk1) approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} %`, 1e-4);
assert.equal(meansOf(e), 5, `T${tier}: 4 plain + 2 strikes ⇒ capped at 5`);
done(h);
}
});
test('S2 军师的手段 (MANUAL, data ACTIVE_RANGE on its 3-13): 15 / 18 s, range 3-13, ASPD +40 / +60, three targets at 155 % / 170 %; 【手段】 up to 8 with 停顿 at 8, back to 5 within 0.1 s after (or once he leaves)', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, FGTY]]) {
const sk = skillOf(tier, elite, S2);
const b = sk.bb;
assert.deepEqual([sk.duration, b.attack_speed, b['attack@max_target'], b['attack@atk_scale'], sk.rangeId], [elite ? 18 : 15, elite ? 60 : 40, 3, elite ? 1.7 : 1.55, '3-13'], `T${tier}`);
const { h, u } = field({ tier, elite, mod, skill: 1 });
assert.deepEqual([u.skill.rule, sk.trigger.rawRule], ['ACTIVE_RANGE', 'DEFAULT'], `T${tier}: the owner's ACTIVE_RANGE rule`);
assert.deepEqual(sk.trigger.customRangeGrid, sk.rangeGrid);
u.skill.gainSp(999);
const far = h.spawn('enemy_dummy', { pos: [10, 8] }); // [0, 3]: only in the 3-13
assert.ok(h.runUntil(() => u.skill.active, 2), `T${tier}: cast with an enemy in the 3-13 only`);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.05);
assert.deepEqual([...u.liveRangeGrid].sort(), [...sk.rangeGrid].sort(), `T${tier}: 3-13 while it runs`);
assert.equal(u.s.aspd, 100 + b.attack_speed + (mod ? 10 : 0), `T${tier}: ASPD`);
const es = [far, h.spawn('enemy_dummy', { pos: [11, 6] }), h.spawn('enemy_dummy', { pos: [9, 7] }), h.spawn('enemy_dummy', { pos: [10, 7] })];
const t0 = h.b.time;
h.run(6);
const per = new Map();
for (const c of atkHits(h, u).filter((x) => x.t > t0)) {
per.set(c.dmg.attackId, (per.get(c.dmg.attackId) ?? new Set()).add(c.target));
const base = u.s.atk * b['attack@atk_scale'];
approx(c.amount, base, `T${tier}: ${b['attack@atk_scale'] * 100} %`, 0.0001);
}
assert.ok(per.size >= 4 && [...per.values()].every((s) => s.size === 3), `T${tier}: three targets per attack`);
const hitOnes = es.filter((e) => meansOf(e) > 0);
assert.ok(hitOnes.some((e) => meansOf(e) === 8), `T${tier}: 8 stacks during S2`);
for (const e of hitOnes) {
assert.ok(meansOf(e) <= 8);
assert.equal(!!e.findBuff(MEANS_SLUG_KEY), meansOf(e) === 8, `T${tier}: 停顿 exactly at 8`);
if (meansOf(e) === 8) assert.ok(e.buffs.some((x) => x.status === 'sluggish'), `T${tier}: a 停顿 status`);
}
h.runUntil(() => !u.skill.active, 20);
h.run(0.12);
for (const e of hitOnes) {
assert.equal(meansOf(e), Math.min(5, meansOf(e)), `T${tier}: back to 5`);
assert.ok(meansOf(e) <= 5 && !e.findBuff(MEANS_SLUG_KEY), `T${tier}: no 停顿 after`);
}
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: 1-1 again`);
done(h);
}
// he leaves the field during S2 ⇒ back to 5 at the next update
const { h, u } = field({ tier: 6, elite: true, mod: FGTY, skill: 1 });
u.skill.gainSp(999);
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.runUntil(() => meansOf(e) === 8, 20);
assert.ok(u.skill.active);
h.b.retreat(u);
h.run(0.12);
assert.equal(meansOf(e), 5, 'he left: 5');
done(h);
});
test('S3 清算 (MANUAL, data CUSTOM_RANGE on x-2): 30 s, ATK +110 / 140 %, ASPD +30 / +40; he 【移动】s onto the first ground target of the x-2 (dodge back to 80 %), leaves 牵绊 on his tile; 35 / 40 % of his hits ×1.4 / 1.5', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, FGTY]]) {
const sk = skillOf(tier, elite, S3);
const b = sk.bb;
assert.deepEqual([sk.duration, b['attack@demetr_s3[bonus].atk'], b['attack@demetr_s3[bonus].attack_speed'], b['attack@demetr_s3[bonus].prob'], b['attack@demetr_s3[bonus].prob_atk_scale'], sk.overrideTokenKey],
[30, elite ? 1.4 : 1.1, elite ? 40 : 30, elite ? 0.4 : 0.35, elite ? 1.5 : 1.4, BOND], `T${tier}`);
const { h, u } = field({ tier, elite, mod, skill: 2, seed: 13 });
assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.trigger.customRangeGrid.length], ['CUSTOM_RANGE', 'CUSTOM_RANGE_SEARCH_ENEMY', 21], `T${tier}: x-2 (21 tiles)`);
h.run(10);
u.skill.gainSp(999);
h.run(1);
assert.equal(u.skill.activations, 0, `T${tier}: no enemy around, no cast`);
assert.ok(u.s.dodgePhys < 0.8, 'his dodge decayed');
const e = h.spawn('enemy_dummy', { pos: [11, 7] }); // [1, 2] of his x-2
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast`);
assert.deepEqual([u.tileR, u.tileC], [11, 7], `T${tier}: moved onto its tile`);
assert.equal(moves(h, u).length, 1, `T${tier}: one 【移动】`);
approx(u.s.dodgePhys, 0.8, `T${tier}: 街头直觉 restarts with the 【移动】`);
approx(u.s.atk, u.base.atk * (1 + b['attack@demetr_s3[bonus].atk']), `T${tier}: ATK +${b['attack@demetr_s3[bonus].atk'] * 100} %`);
assert.equal(u.s.aspd, 100 + b['attack@demetr_s3[bonus].attack_speed'] + (mod ? 10 : 0), `T${tier}: ASPD`);
const bond = bondOf(h);
assert.ok(bond, `T${tier}: 牵绊`);
assert.deepEqual([bond.tileR, bond.tileC, bond.ownerUnit === u, bond.s.flags.invulnerable, bond.s.flags.untargetable, bond.s.flags.isolated, bond.profile.noAttack, bond.s.blockCnt],
[10, 5, true, true, true, true, true, 0], `T${tier}: on his tile, 无敌 / untargetable / 孤立, no attack, blocks nothing`);
assert.equal(h.b.isReservedTile(10, 5), true, `T${tier}: nobody else deploys there`);
// the crits
const t0 = h.b.time;
u.skill.extend(200);
h.run(150);
const hits = atkHits(h, u).filter((c) => c.t > t0 && c.target === e);
const atk = u.s.atk;
const big = hits.filter((c) => Math.abs(c.amount - atk * b['attack@demetr_s3[bonus].prob_atk_scale'] * (c.dmg.mul ?? 1)) < 1e-3 * atk).length;
const plain = hits.filter((c) => Math.abs(c.amount - atk * (c.dmg.mul ?? 1)) < 1e-3 * atk).length;
assert.equal(big + plain, hits.length, `T${tier}: every hit is ×1 or ×${b['attack@demetr_s3[bonus].prob_atk_scale']}`);
assert.ok(Math.abs(big / hits.length - b['attack@demetr_s3[bonus].prob']) < 0.06, `T${tier}: ${big} / ${hits.length} ≈ ${b['attack@demetr_s3[bonus].prob']}`);
done(h);
}
});
test('S3 清算 targets: ground enemies only, on a tile he may take (melee-buildable low ground, nobody but him / 牵绊) — else he 【移动】s in place once; another target 1 s after his last damage, at once when the elite he attacked falls; moving home retreats 牵绊 first', () => {
// (a) a flyer and an enemy on a floor tile (build NONE): an in-place move, 牵绊 never placed
{
const rows = { 11: '##hrrrfrrrfrrrrrrrf##' }; // (11, 6) = floor
const { h, u } = field({ tier: 6, elite: true, mod: FGTY, skill: 2, rows });
assert.equal(h.b.grid.canStand(11, 6), false);
h.spawn('enemy_fly', { pos: [10, 6] });
const onFloor = h.spawn('enemy_dummy', { pos: [11, 6] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1), 'cast (the flyer counts for CUSTOM_RANGE)');
assert.deepEqual([u.tileR, u.tileC, moves(h, u).length, !!bondOf(h)], [10, 5, 1, false], 'an in-place 【移动】, no 牵绊');
assert.ok(onFloor.alive);
done(h);
}
// (b) the idle re-target, the elite re-target, home
{
const { h, u } = field({ tier: 6, elite: true, mod: FGTY, skill: 2 });
const e1 = h.spawn('enemy_dummy', { pos: [11, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
assert.deepEqual([u.tileR, u.tileC], [11, 7]);
h.run(2);
h.b.kill(e1, null);
const last = Math.max(...atkHits(h, u).map((c) => c.t));
const e2 = h.spawn('enemy_dummy', { pos: [9, 6] });
assert.ok(h.runUntil(() => u.tileR === 9 && u.tileC === 6, 3), 'the next target');
assert.ok(h.b.time >= last + 1 - 1e-6 && h.b.time <= last + 1 + 0.1, `1 s after his last damage (${h.b.time} vs ${last})`);
assert.equal(moves(h, u).length, 2);
assert.deepEqual([bondOf(h).tileR, bondOf(h).tileC], [10, 5], 'one 牵绊, still on his tile');
assert.equal(h.b.allyUnits.filter((a) => a.kind === 'token' && a.alive).length, 1);
// the elite he attacks falls ⇒ at once (the next tick), not 1 s later
h.b.kill(e2, null);
const el = h.spawn('enemy_elite', { pos: [9, 6] }); // on his tile
h.runUntil(() => atkHits(h, u).some((c) => c.target === el), 2);
const home = h.spawn('enemy_dummy', { pos: [10, 5] }); // on 牵绊's tile: his located tile
h.b.kill(el, null);
const tk = h.b.time;
assert.ok(h.runUntil(() => u.tileR === 10 && u.tileC === 5, 0.2), 'back home on the next tick');
assert.ok(h.b.time - tk < 0.05, 'at once');
assert.equal(bondOf(h), null, '牵绊 retreated first (his located tile)');
assert.ok(home.alive);
// the end: a 【返回】 in place, the SP emptied, a short 不死
h.runUntil(() => !u.skill.active, 40);
assert.deepEqual([u.tileR, u.tileC], [10, 5]);
assert.ok(u.skill.sp < 0.1, `SP emptied (${u.skill.sp})`);
assert.equal(moves(h, u).length, 4, 'cast, idle, elite, return');
done(h);
}
});
test('S3 清算: a lethal hit while it runs ⇒ 不死 / untargetable / no block / 禁疗, the skill ends at once and he returns (a short 不死 after); away without 牵绊 at the end ⇒ forced retreat', () => {
{
const { h, u } = field({ tier: 5, elite: false, skill: 2 });
const e = h.spawn('enemy_dummy', { pos: [11, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
assert.deepEqual([u.tileR, u.tileC], [11, 7]);
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(u.alive, 'not knocked out');
assert.ok(u.hp >= 1 && u.hp <= 1.0001, 'held at 1 HP');
assert.deepEqual([u.s.flags.untargetable, u.s.flags.noBlock, u.s.flags.healFree], [true, true, true], 'demetr_s3[undeadable]');
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(u.alive, 'still 不死 until the skill ends');
h.step();
h.step();
assert.ok(!u.skill.active, 'the skill ended');
assert.deepEqual([u.tileR, u.tileC, !!bondOf(h), !!u.findBuff(UNDEAD_KEY)], [10, 5, false, false], 'returned home');
assert.equal(h.hooksOf('skillEnd').find((c) => c.unit === u)?.reason, 'fatal');
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(u.alive, `a short 不死 after the return (${RETURN_UNDYING} s)`);
h.run(RETURN_UNDYING + 0.05);
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(!u.alive, 'then a knock-out');
done(h);
}
{
const { h, u } = field({ tier: 6, elite: true, mod: FGTY, skill: 2 });
h.spawn('enemy_dummy', { pos: [9, 6] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
h.b.retreat(bondOf(h), { reason: 'expired', permanent: true });
h.runUntil(() => !u.skill.active, 40);
assert.ok(!u.alive && u.removeReason === 'retreat', 'forced retreat');
assert.ok(h.b.isDown(u), 'lies down and redeploys by the normal rule');
done(h);
}
// 牵绊 belongs to S3: never under S1 / S2
for (const skill of [0, 1]) {
const { h, u } = field({ tier: 6, elite: true, mod: FGTY, skill });
for (const pos of [[10, 6], [11, 7], [9, 6]]) h.spawn('enemy_dummy', { pos });
u.skill.gainSp(999);
h.run(30);
assert.equal(h.b.allyUnits.filter((a) => a.kind === 'token').length, 0, `S${skill + 1}: no 牵绊`);
assert.equal(moves(h, u).length, 0, `S${skill + 1}: no 【移动】`);
done(h);
}
});
+363
View File
@@ -0,0 +1,363 @@
// test/content/op_lessng.test.js — the 自选 operator kit of 止颂 (char_4011_lessng, 6★ 无畏者; kit
// server/sim/content/kits/ops/op-lessng.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, DRE-X
// “沉锋之束” or DRE-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_lessng.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 { DUEL_KEY, OATH_KEY, PAIN_KEY, REBORN_KEY } from '../../server/sim/content/kits/ops/op-lessng.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 LESSNG = 'char_4011_lessng';
const FORMS = BACKUPS.units[LESSNG].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const DREX = 'uniequip_002_lessng', DREY = 'uniequip_003_lessng';
const S1 = 'skchr_lessng_1', S2 = 'skchr_lessng_2', S3 = 'skchr_lessng_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 talentOf = (u, i) => u.def.raw.talents.find((t) => t.index === i)?.bb ?? {};
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, DREX, DREY].map((m) => [t, true, m]))];
/** A plain ally (no kit, no damage modifiers) for friendly-damage checks. */
const PLAIN = chessRec({ id: 'test_plain_a', name: '测试', skill: null });
/** A battle with 止颂 as uid 1 at (row, col) facing RIGHT, plus `others`. */
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES, chess: { test_plain_a: PLAIN } }, timeLimit: 600, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: LESSNG, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
/** 止颂 on row 9 with a walker blocked by her. */
function blocked(o = {}) {
const r = field({ row: 9, ...o });
const w = r.h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(r.h.runUntil(() => w.blockedBy === r.u, 30), 'the walker reaches her');
return { ...r, w };
}
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
const noSp = (h, u) => h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } }); // 阻回: the MANUAL skill stays off
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
test('止颂 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, blocks 1, melee ground-only, 1.5 s, 莱塔尼亚, no 特质', () => {
assert.equal(OPERATOR_KITS[LESSNG], KITS[LESSNG]);
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], [LESSNG, 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.base.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.sub, u.base.bat, u.dmgType], [1, 'melee', false, 'fearless', 1.5, 'phys'], `${label(f)}: 无畏者`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.raw.nationId], [['emptyShip'], [], 'leithanien'], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
done(h);
}
}
// E2 Lv1 2798 / 866 / 232, E2 Lv60 3384 / 984 / 262; DRE-X +230 / +17 / +17 → +400 / +30 / +30; DRE-Y +300 / +55 → +450 / +80
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], [2798, 866, 3384, 984]);
assert.deepEqual([modOf(5, DREX).attr, modOf(6, DREX).attr, modOf(5, DREY).attr, modOf(6, DREY).attr],
[{ maxHp: 230, atk: 17, def: 17 }, { maxHp: 400, atk: 30, def: 30 }, { maxHp: 300, atk: 55 }, { maxHp: 450, atk: 80 }]);
});
test('a 自选 pick: 止颂 is offered at tiers 5 and 6 and a roster with him passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(LESSNG));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(LESSNG), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: LESSNG, skillIndex: 1, uniEquipId: DREY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: LESSNG, skillIndex: 1, uniEquipId: DREY } } });
});
test('T1 苦痛专注: while he blocks, physical / arts damage from anyone but an enemy he blocks — 无来源, allies, himself included — ×0.65 (DRE-Y stage 3: ×0.6); none while not blocking, none on true damage', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y3 = elite && mod === DREY && tier === 6;
const cut = talentOf(field({ tier, elite, mod }).u, 0).damage_resistance;
assert.equal(cut, y3 ? 0.4 : 0.35, `${label(f)}: the talent of the form`);
const { h, u, w } = blocked({ tier, elite, mod, others: [{ uid: 2, chessId: 'test_plain_a', row: 11, col: 3 }] });
const ally = h.unit(2);
const other = h.spawn('enemy_dummy', { pos: [11, 8] });
const hit = (src, d) => { u.hp = u.s.maxHp; return h.b.dealDamage(src, u, d); }; // (full HP: the return value is the HP removed)
const arts = (src) => hit(src, { amount: 1000, type: 'arts' });
approx(arts(other), 1000 * (1 - cut), `${label(f)}: an enemy he does not block`);
approx(arts(w), 1000, `${label(f)}: the enemy he blocks`);
approx(arts(null), 1000 * (1 - cut), `${label(f)}: 无来源`);
approx(arts(ally), 1000 * (1 - cut), `${label(f)}: an ally`);
approx(arts(u), 1000 * (1 - cut), `${label(f)}: himself`);
approx(hit(other, { amount: 1000, type: 'true' }), 1000, `${label(f)}: true damage`);
approx(hit(other, { amount: 2000, type: 'phys' }), (2000 - u.s.def) * (1 - cut), `${label(f)}: physical`);
u.hp = u.s.maxHp;
h.b.releaseBlocked(u);
h.b.kill(w, null);
h.step();
assert.equal(u.blocking.length, 0);
approx(arts(other), 1000, `${label(f)}: not while he blocks nobody`);
done(h);
}
});
test('T2 痛楚砺刃: any damage aimed at him (a dodged one too) ⇒ ATK +12 % (DRE-X stage 3: +20 %) for 15 s, refreshed, never stacked', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod });
const t = talentOf(u, 1);
const x3 = elite && mod === DREX && tier === 6;
assert.deepEqual([t.atk, t.add_atk_duration], [x3 ? 0.2 : 0.12, 15], `${label(f)}: the talent of the form`);
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
assert.equal(u.findBuff(PAIN_KEY), null);
h.b.addBuff(u, { key: 'test:dodge', mods: { dodgePhys: 1 } });
assert.equal(h.b.dealDamage(e, u, { amount: 500, type: 'phys' }), 0, `${label(f)}: dodged`);
assert.deepEqual(u.findBuff(PAIN_KEY)?.mods, { atkPct: t.atk }, `${label(f)}: still triggered`);
approx(u.s.atk, u.base.atk * (1 + t.atk), `${label(f)}: ATK +${t.atk * 100} %`);
h.b.removeBuff(u, 'test:dodge');
h.run(10);
h.b.dealDamage(e, u, { amount: 100, type: 'arts' });
h.b.dealDamage(null, u, { amount: 100, type: 'true' });
approx(u.findBuff(PAIN_KEY).timeLeft, 15, `${label(f)}: refreshed to 15 s`, 0.01);
approx(u.s.atk, u.base.atk * (1 + t.atk), `${label(f)}: not stacked`);
h.run(15.1);
assert.equal(u.findBuff(PAIN_KEY), null, `${label(f)}: gone 15 s after the last hit`);
done(h);
}
});
test('S1 强力击·γ型 (AUTO, attack SP 3, data DEFAULT): the next attack at 205 % / 225 % ATK (× DRE-X 115 % on a blocked target)', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREX], [5, true, DREY]]) {
const sk = skillOf(tier, elite, S1);
const { h, u, w } = blocked({ tier, elite, mod, skill: 0 });
assert.deepEqual([u.skill.rule, u.skill.spType, u.skill.spCost, sk.skillType, sk.bb.atk_scale], ['DEFAULT', 'attack', 3, 'AUTO', elite ? 2.25 : 2.05], `T${tier}`);
const scale = mod === DREX ? modOf(tier, DREX).traitOverride.bb.atk_scale : 1;
const t0 = h.b.time;
assert.ok(h.runUntil(() => atkHits(h, u).some((c) => c.dmg.isSkill && c.t > t0), 20), `T${tier}: cast`);
const hits = atkHits(h, u).filter((c) => c.t > t0 && c.target === w);
const sk1 = hits.find((c) => c.dmg.isSkill);
approx(sk1.amount, u.s.atk * sk.bb.atk_scale * scale, `T${tier}: ${sk.bb.atk_scale * 100} %`);
for (const c of hits.filter((x) => !x.dmg.isSkill)) approx(c.amount, u.s.atk * scale, `T${tier}: plain attacks`);
done(h);
}
});
test('S2 虔修对决 (PASSIVE, 部署后): for 18 / 21 s from every deployment ATK +25 / 35 %, two strikes per attack and 苦痛专注 ×2; then single strikes', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREY], [6, true, DREX]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.skillType, sk.spType, sk.duration, sk.bb.atk, sk.bb.talent_scale], ['PASSIVE', 'ON_DEPLOY', elite ? 21 : 18, elite ? 0.35 : 0.25, 2], `T${tier}`);
const { h, u, w } = blocked({ tier, elite, mod, skill: 1 });
const cut = talentOf(u, 0).damage_resistance;
assert.ok(h.b.time < sk.duration, 'the walker arrived inside the window');
assert.deepEqual(u.findBuff(DUEL_KEY)?.mods, { atkPct: sk.bb.atk }, `T${tier}: ATK +${sk.bb.atk * 100} %`);
const other = h.spawn('enemy_dummy', { pos: [11, 8] });
approx(h.b.dealDamage(other, u, { amount: 1000, type: 'arts' }), 1000 * (1 - cut * 2), `T${tier}: 苦痛专注 ×2`);
const t0 = h.b.time;
h.runUntil(() => !u.findBuff(DUEL_KEY), 30);
approx(h.b.time, sk.duration, `T${tier}: the window ends ${sk.duration} s after the deployment`, 0.05);
const inWin = new Map();
for (const c of atkHits(h, u).filter((x) => x.t > t0)) inWin.set(c.dmg.attackId, (inWin.get(c.dmg.attackId) ?? 0) + 1);
assert.ok(inWin.size >= 2 && [...inWin.values()].every((n) => n === 2), `T${tier}: two strikes per attack (${[...inWin.values()]})`);
const t1 = h.b.time;
h.run(5);
const after = new Map();
for (const c of atkHits(h, u).filter((x) => x.t > t1)) after.set(c.dmg.attackId, (after.get(c.dmg.attackId) ?? 0) + 1);
assert.ok(after.size >= 2 && [...after.values()].every((n) => n === 1), `T${tier}: single strikes after`);
approx(u.s.atk, u.base.atk * (1 + (u.findBuff(PAIN_KEY) ? talentOf(u, 1).atk : 0)), `T${tier}: ATK back`);
approx(h.b.dealDamage(other, u, { amount: 1000, type: 'arts' }), 1000 * (1 - cut), `T${tier}: 苦痛专注 ×1`);
// a redeploy restarts the window
h.b.retreat(u);
h.b.redeploy(u);
h.step();
approx(u.findBuff(DUEL_KEY)?.timeLeft ?? 0, sk.duration, `T${tier}: again for ${sk.duration} s`, 0.05);
assert.equal(w.alive, true);
done(h);
}
});
test('S3 苦修破誓 (MANUAL, data DEFAULT): 20 s, max HP +65 / 80 %, a blocked target at 160 % / 180 % ATK (× DRE-X), no 异常状态 while it runs; then all back', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREX], [5, true, DREY]]) {
const sk = skillOf(tier, elite, S3);
const b = sk.bb;
assert.deepEqual([sk.duration, b.max_hp, b['lessng_s3[atk_scale].atk_scale'], b.magical_value, sk.skillType], [20, elite ? 0.8 : 0.65, elite ? 1.8 : 1.6, 600, 'MANUAL'], `T${tier}`);
const { h, u, w } = blocked({ tier, elite, mod, skill: 2 });
assert.equal(u.skill.rule, 'DEFAULT');
const unb = h.spawn('enemy_dummy', { pos: [9, 5] }); // on his tile beside the walker: he blocks only one
const hp0 = u.hp, max0 = u.s.maxHp;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
approx(u.skill.timeLeft, 20, `T${tier}: 20 s`, 0.05);
approx(u.s.maxHp, max0 * (1 + b.max_hp), `T${tier}: max HP +${b.max_hp * 100} %`);
approx(u.hp / u.s.maxHp, hp0 / max0, `T${tier}: ratio kept`);
for (const st of ['stun', 'cold', 'freeze', 'sleep', 'silence', 'fear']) assert.equal(h.b.applyStatus(u, st, { duration: 3, source: w }), false, `T${tier}: immune to ${st}`);
const scale = mod === DREX ? modOf(tier, DREX).traitOverride.bb.atk_scale : 1;
const t0 = h.b.time;
h.run(6);
const hits = atkHits(h, u).filter((c) => c.t > t0);
assert.ok(hits.length >= 3);
for (const c of hits) {
if (c.target === w) approx(c.amount, u.s.atk * b['lessng_s3[atk_scale].atk_scale'] * scale, `T${tier}: blocked ×${b['lessng_s3[atk_scale].atk_scale']}`);
else approx(c.amount, unb.blockedBy ? u.s.atk * b['lessng_s3[atk_scale].atk_scale'] * scale : u.s.atk, `T${tier}: the other target`);
}
h.runUntil(() => !u.skill.active, 20);
h.step();
assert.equal(u.findBuff(OATH_KEY), null);
approx(u.s.maxHp, max0, `T${tier}: max HP back`);
assert.equal(h.b.applyStatus(u, 'stun', { duration: 0.5, source: w }), true, `T${tier}: stuns land again`);
done(h);
}
});
test('S3 苦修破誓 cast under a 异常状态: 600 arts to himself first (苦痛专注 applies while he blocks; may knock him out), every 异常状态 cleansed, then the HP; 沉默 does not hold the cast back, 晕眩 does (no attack)', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, DREX]]) {
const cut = 0.35;
// cold: he still attacks, the basic strategy casts; he blocks ⇒ 600 × 0.65
{
const { h, u, w } = blocked({ tier, elite, mod, skill: 2 });
h.b.applyStatus(u, 'cold', { duration: 20, source: w });
const hp0 = u.hp, max0 = u.s.maxHp;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 5), `T${tier}: cast while cold`);
const self = h.hooksOf('damaged').filter((c) => c.source === u && c.target === u);
assert.equal(self.length, 1, `T${tier}: one self hit`);
assert.deepEqual([self[0].type, Math.round(self[0].amount)], ['arts', Math.round(600 * (1 - cut))], `T${tier}: 600 arts ×0.65`);
assert.ok(!u.buffs.some((x) => x.status === 'cold'), `T${tier}: cold cleansed`);
approx(u.hp, (hp0 - 600 * (1 - cut)) / max0 * u.s.maxHp, `T${tier}: the HP buff after the damage (ratio of the hurt HP)`);
assert.ok(u.findBuff(PAIN_KEY), `T${tier}: 痛楚砺刃 saw it`);
done(h);
}
// 沉默: the engine's basic strategy refuses a silenced cast — this skill "可以无视异常状态开启"
{
const { h, u } = field({ tier, elite, mod, skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.applyStatus(u, 'silence', { duration: 20, source: null });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 5), `T${tier}: cast while silenced`);
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u)?.reason, 'abnormal');
assert.equal(Math.round(h.hooksOf('damaged').find((c) => c.source === u && c.target === u)?.amount), 600, `T${tier}: 600 arts (he blocks nobody)`);
assert.ok(!u.s.flags.silence, `T${tier}: silence cleansed`);
done(h);
}
// 晕眩: no attack ⇒ no cast until it ends (then no abnormal state, no self damage)
{
const { h, u } = field({ tier, elite, mod, skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.applyStatus(u, 'stun', { duration: 3, source: null });
u.skill.gainSp(999);
h.run(2.5);
assert.equal(u.skill.activations, 0, `T${tier}: not while stunned`);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: once the stun ends`);
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.target === u).length, 0, `T${tier}: no self damage`);
done(h);
}
// the self damage may knock him out (可致死)
{
const { h, u } = field({ tier, elite, mod, skill: 2 });
h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.applyStatus(u, 'cold', { duration: 20, source: null });
u.hp = 300;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => !u.alive, 5), `T${tier}: knocked out by his own 600`);
assert.equal(u.findBuff(OATH_KEY), null);
done(h);
}
}
});
test('DRE-X “沉锋之束”: his attacks ×115 % on a target blocked by anyone (another operator\'s too), ×1 on an unblocked one — stages 1 and 3; none without it', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 6 }] });
noSp(h, u);
const yak = h.unit(2);
const e = h.spawn('enemy_dummy', { pos: [10, 6] }); // blocked by 角峰, on his front tile
h.step();
assert.equal(e.blockedBy, yak);
const x = elite && mod === DREX;
if (x) assert.equal(u.def.raw.trait.bb.atk_scale, 1.15, label(f));
h.run(6);
const hits = atkHits(h, u).filter((c) => c.target === e);
assert.ok(hits.length >= 3, label(f));
for (const c of hits) approx(c.amount, u.s.atk * (x ? 1.15 : 1), `${label(f)}: ×${x ? 1.15 : 1}`);
h.b.releaseBlocked(yak);
h.b.retreat(yak);
h.step();
assert.ok(!e.blockedBy);
const t0 = h.b.time;
h.run(4);
for (const c of atkHits(h, u).filter((y) => y.t > t0)) approx(c.amount, u.s.atk, `${label(f)}: unblocked ×1`);
done(h);
}
});
test('DRE-Y “苦修者的抗压训练”: the first lethal hit of a deployment ⇒ max HP ×0.4, ASPD +30, full HP (once; again after a redeploy; not while a 不死 holds him); stage 3: 12 % of DEF ignored on the enemy he blocks', () => {
for (const tier of [5, 6]) {
const { h, u } = field({ tier, elite: true, mod: DREY });
const tb = u.def.raw.trait.bb;
assert.deepEqual([tb.attack_speed, tb.max_hp, tb.hp_ratio, tb.value], [30, 0.4, 1, 0.6], `T${tier}: trait bb`);
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
const max0 = u.s.maxHp, aspd0 = u.s.aspd;
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(u.alive, `T${tier}: not knocked out`);
approx(u.s.maxHp, max0 * 0.4, `T${tier}: max HP ×0.4`);
approx(u.hp, u.s.maxHp, `T${tier}: full HP of the new max`);
assert.equal(u.s.aspd, aspd0 + 30, `T${tier}: ASPD +30`);
assert.ok(u.findBuff(REBORN_KEY));
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(!u.alive, `T${tier}: once per deployment`);
h.b.redeploy(u);
h.step();
approx(u.s.maxHp, max0, `T${tier}: a new deployment: max HP back`);
h.b.dealDamage(e, u, { amount: 1e7, type: 'true' });
assert.ok(u.alive, `T${tier}: again after the redeploy`);
done(h);
// a running 不死 (坚固维式重锤's window) holds him instead: the module is not used
const r = field({ tier, elite: true, mod: DREY });
r.u.mem.undyingAt = `${r.u.deploySeq}:0`;
r.u.mem.undyingUntil = r.h.b.time + 5;
const hammer = r.h.b.on('fatal', (c) => { if (c.unit === r.u && !c.prevented) c.prevented = true; }, { priority: -99 });
const f = r.h.spawn('enemy_dummy', { pos: [11, 8] });
r.h.b.dealDamage(f, r.u, { amount: 1e7, type: 'true' });
assert.ok(r.u.alive && r.u.hp < 2 && !r.u.findBuff(REBORN_KEY), `T${tier}: held by the 不死, the module untouched`);
r.h.b.off(hammer);
r.u.mem.undyingUntil = 0;
r.h.b.dealDamage(f, r.u, { amount: 1e7, type: 'true' });
assert.ok(r.u.alive && r.u.findBuff(REBORN_KEY), `T${tier}: the module afterwards`);
done(r.h);
}
// stage 3's hidden part: 无视目标12%的防御力 on the enemy he blocks (none at stage 1, none on an unblocked enemy)
for (const tier of [5, 6]) {
const ENEMY_DEF = 1000;
const h = makeBattle({
defs: { enemies: { enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0, def: ENEMY_DEF }), enemy_dummy: dummy('enemy_dummy', { def: ENEMY_DEF }) } },
timeLimit: 600, autoFinish: false, seed: 5, flags: { dpPerSec: 0 }, hooks: ['damaged'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: LESSNG, skillIndex: 2, uniEquipId: DREY }, elite: true, row: 9, col: 5 }],
});
h.step();
const u = h.unit(1);
noSp(h, u);
const w = h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(h.runUntil(() => w.blockedBy === u, 30));
const t0 = h.b.time;
h.run(6);
const pen = tier === 6 ? 0.12 : 0;
const hits = atkHits(h, u).filter((c) => c.t > t0 && c.target === w);
assert.ok(hits.length >= 3);
for (const c of hits) approx(c.amount, Math.max(u.s.atk - ENEMY_DEF * (1 - pen), u.s.atk * 0.05), `T${tier}: DEF ×${1 - pen}`);
done(h);
}
});
+324
View File
@@ -0,0 +1,324 @@
// test/content/op_pallas.test.js — the 自选 operator kit of 帕拉斯 (char_485_pallas, 6★ 教官; kit
// server/sim/content/kits/ops/op-pallas.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, INS-X
// “故乡的山岩” or INS-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_pallas.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 { PEAK_KEY, holderKey } from '../../server/sim/content/kits/ops/op-pallas.js';
import { diyPool, validateDiyPicks, diyRecord } from '../../shared/diy.js';
import { positionClass } from '../../server/match/board.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 PALLAS = 'char_485_pallas';
const FORMS = BACKUPS.units[PALLAS].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const INSX = 'uniequip_002_pallas', INSY = 'uniequip_003_pallas';
const S1 = 'skchr_pallas_1', S2 = 'skchr_pallas_2', S3 = 'skchr_pallas_3';
/** The unit form of a slot (tier, normal / elite). */
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);
/** A talent of the composed record the unit fights with (module changes applied). */
const talentOf = (u, i) => u.def.raw.talents.find((t) => t.index === i)?.bb ?? {};
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }) };
/** A 米诺斯 operator of no kit (帕拉斯 is the only one of the data): a plain melee record with nationId minos. */
const MINOS_OP = { ...chessRec({ id: 'test_minos_a', name: '米诺斯测试', stats: { maxHp: 2000, atk: 500, def: 200, blockCnt: 1 }, skill: null }), nationId: 'minos' };
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, INSX, INSY].map((m) => [t, true, m]))];
/** 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, rows = null } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES, chess: { test_minos_a: MINOS_OP } }, timeLimit: 600, autoFinish: false, seed,
...(rows ? { stageId: undefined, flat: { rows } } : {}),
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'heal', 'skillStart', 'skillEnd', 'statusApplied'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: PALLAS, 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);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** The instructor scale of a form (trait bb atk_scale; INS-X's override). */
const traitScale = (tier, elite, mod) => (elite && mod ? modOf(tier, mod).traitOverride.bb.atk_scale : formOf(tier, elite).trait.bb.atk_scale);
test('帕拉斯 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 2-2, blocks 2, melee ground-only, 教官 ×1.2 (INS-X ×1.3), 米诺斯, no 特质', () => {
assert.equal(OPERATOR_KITS[PALLAS], KITS[PALLAS]);
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], [PALLAS, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd],
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.aspd + (m?.attr.aspd ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.sub, u.base.bat, u.dmgType], [2, 'melee', false, 'instructor', 1.05, 'phys'], `${label(f)}: 教官`);
approx(u.profile.unblockedScale, traitScale(tier, elite, mod), `${label(f)}: 教官 scale`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 2-2`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.raw.nationId], [['emptyShip'], [], 'minos'], `${label(f)}: bonds / 特质 / nation`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1413 / 577 / 382, E2 Lv60 1778 / 650 / 430; INS-X +150 HP +55 ATK → +210 / +83,
// trait 130 %; INS-Y +40 ATK +3 ASPD → +60 / +5
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/1/4/0'].stats.def, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1413, 577, 382, 1778, 650]);
assert.deepEqual([modOf(5, INSX).attr, modOf(6, INSX).attr, modOf(5, INSY).attr, modOf(6, INSY).attr], [{ maxHp: 150, atk: 55 }, { maxHp: 210, atk: 83 }, { atk: 40, aspd: 3 }, { atk: 60, aspd: 5 }]);
assert.deepEqual([formOf(5, false).trait.bb.atk_scale, modOf(5, INSX).traitOverride.bb.atk_scale, modOf(6, INSY).traitOverride.bb.atk_scale], [1.2, 1.3, 1.2]);
});
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(PALLAS));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(PALLAS), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: PALLAS, skillIndex: 2, uniEquipId: INSY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: PALLAS, skillIndex: 2, uniEquipId: INSY } } });
});
test('trait 教官: her attack ×1.2 (INS-X ×1.3) on an enemy she does not block, ×1 on the one she blocks; flyers are out of her reach', () => {
for (const f of [[5, false, null], [5, true, INSX], [6, true, INSX], [6, true, INSY]]) {
const [tier, elite, mod] = f;
const scale = traitScale(tier, elite, mod);
const noSp = (h, u) => h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } }); // (阻回: S3 stays off)
{
const { h, u } = field({ tier, elite, mod, skill: 2 });
noSp(h, u);
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
const fly = h.spawn('enemy_fly', { pos: [10, 6] });
h.run(6);
const hits = atkHits(h, u);
assert.ok(hits.length >= 3, `${label(f)}: she attacks (${hits.length})`);
for (const c of hits) {
assert.equal(c.target, e, `${label(f)}: only the ground dummy`);
approx(c.amount, u.s.atk * scale, `${label(f)}: unblocked ×${scale}`);
assert.equal(c.type, 'phys');
}
assert.ok(!h.hooksOf('damaged').some((c) => c.target === fly), `${label(f)}: no anti-air`);
done(h);
}
{
const { h, u } = field({ tier, elite, mod, skill: 2, row: 9 });
noSp(h, u);
const w = h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(h.runUntil(() => w.blockedBy === u, 30), `${label(f)}: the walker reaches her`);
const t0 = h.b.time;
h.run(6);
const hits = atkHits(h, u).filter((c) => c.t > t0);
assert.ok(hits.length >= 3, `${label(f)}: she attacks the enemy she blocks`);
for (const c of hits) approx(c.amount, u.s.atk, `${label(f)}: blocked ×1`);
done(h);
}
}
});
test('T1 英雄的诞生: while she is deployed, every 米诺斯 operator (her too) ATK +25 % above 80 % HP (INS-X stage 3: above 50 %, +30 %) — one 精力充沛, not for others, gone when she leaves', () => {
const others = [{ uid: 2, chessId: 'test_minos_a', row: 11, col: 3 }, { uid: 3, chessId: 'chess_char_1_02_a', row: 12, col: 7 }];
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2, others });
h.step();
const minos = h.unit(2), yak = h.unit(3);
const t = talentOf(u, 0);
const x3 = elite && mod === INSX && tier === 6;
assert.deepEqual([t['peak_performance.hp_ratio'], t['peak_performance.atk']], x3 ? [0.5, 0.3] : [0.8, 0.25], `${label(f)}: the talent of the form`);
const v = t['peak_performance.atk'], thr = t['peak_performance.hp_ratio'];
for (const a of [u, minos]) {
assert.deepEqual(a.findBuff(PEAK_KEY)?.mods, { atkPct: v }, `${label(f)}: ${a.name}`);
approx(a.s.atk, a.base.atk * (1 + v), `${label(f)}: ${a.name} ATK`);
}
assert.equal(yak.findBuff(PEAK_KEY), null, `${label(f)}: 角峰 (谢拉格) gets nothing`);
// the HP condition, re-read every tick: strictly above hp_ratio
minos.hp = minos.s.maxHp * thr;
h.step();
assert.equal(minos.findBuff(PEAK_KEY), null, `${label(f)}: at ${thr * 100} % HP`);
minos.hp = minos.s.maxHp * 0.6;
h.step();
assert.equal(!!minos.findBuff(PEAK_KEY), x3, `${label(f)}: at 60 % HP`);
minos.hp = minos.s.maxHp;
h.step();
assert.ok(minos.findBuff(PEAK_KEY), `${label(f)}: back above`);
h.b.retreat(u);
h.step();
assert.equal(minos.findBuff(PEAK_KEY), null, `${label(f)}: gone once she leaves the field`);
done(h);
}
});
test('T2 女神的振奋: every damage instance she deals heals her and the ally on the tile in front 40 HP (INS-Y stage 3: 60, and every 米诺斯 operator — her too — 40 more), through 禁疗, not a 不成为治疗目标 ally, not the one behind', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const others = [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 6 }, { uid: 3, chessId: 'test_minos_a', row: 10, col: 4 }];
const { h, u } = field({ tier, elite, mod, skill: 2, others });
const front = h.unit(2), behind = h.unit(3);
const t = talentOf(u, 1);
const y3 = elite && mod === INSY && tier === 6;
assert.deepEqual([t.value, t['pallas_e_t_2.value']], y3 ? [60, 40] : [40, undefined], `${label(f)}: the talent of the form`);
for (const a of [u, front, behind]) a.hp = a.s.maxHp * 0.5;
h.b.addBuff(front, { key: 'test:healFree', flags: { healFree: true } }); // 禁疗: the heal ignores it
h.spawn('enemy_dummy', { pos: [10, 7] });
h.run(5);
const out = h.hooksOf('damaged').filter((c) => c.source === u && c.target.side === 'enemy');
assert.ok(out.length >= 3, `${label(f)}: ${out.length} damage instances`);
const heals = (a, amount) => h.hooksOf('heal').filter((c) => c.source === u && c.target === a && Math.abs(c.amount - amount) < 1e-6).length;
assert.equal(heals(u, t.value), out.length, `${label(f)}: herself ${t.value} per instance`);
assert.equal(heals(front, t.value), out.length, `${label(f)}: the ally in front (禁疗 too)`);
assert.equal(heals(behind, t.value), 0, `${label(f)}: not the ally behind`);
const extra = (a) => h.hooksOf('heal').filter((c) => c.source === u && c.target === a && c.opts?.tags?.includes('module')).length;
assert.deepEqual([extra(u), extra(behind), extra(front)], y3 ? [out.length, out.length, 0] : [0, 0, 0], `${label(f)}: the 米诺斯 extra (INS-Y stage 3): her and the 米诺斯 operator behind`);
if (y3) assert.ok(h.hooksOf('heal').filter((c) => c.opts?.tags?.includes('module')).every((c) => Math.abs(c.amount - 40) < 1e-6), `${label(f)}: 40 each`);
assert.ok(front.hp > front.s.maxHp * 0.5, `${label(f)}: the front ally gained HP`);
// 不成为其他角色的治疗目标 (noHeal): not healed
const n = h.hooksOf('heal').length;
h.b.addBuff(front, { key: 'test:noHeal', flags: { noHeal: true } });
const hp = front.hp;
h.run(3);
assert.equal(front.hp, hp, `${label(f)}: no heal reaches a noHeal ally`);
assert.ok(h.hooksOf('heal').length > n, `${label(f)}: she still heals herself`);
done(h);
}
});
test('S1 胜利的连击 (AUTO, attack SP 3, data DEFAULT): the next attack strikes twice for 125 % / 145 % ATK, each ×the 教官 scale, and heals twice', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, INSX], [5, true, INSY]]) {
const sk = skillOf(tier, elite, S1);
const { h, u } = field({ tier, elite, mod, skill: 0 });
assert.deepEqual([u.skill.rule, u.skill.spType, u.skill.spCost, sk.initSp, sk.skillType, sk.bb.atk_scale], ['DEFAULT', 'attack', 3, 0, 'AUTO', elite ? 1.45 : 1.25], `T${tier}`);
h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.activations === 1 && atkHits(h, u).some((c) => c.dmg.isSkill), 20), `T${tier}: cast on her 4th attack`);
const sk1 = atkHits(h, u).filter((c) => c.dmg.isSkill);
const id = sk1[0].dmg.attackId;
const strikes = sk1.filter((c) => c.dmg.attackId === id);
assert.equal(strikes.length, 2, `T${tier}: two strikes`);
for (const c of strikes) approx(c.amount, u.s.atk * sk.bb.atk_scale * traitScale(tier, elite, mod), `T${tier}: ${sk.bb.atk_scale * 100} % × 教官`);
assert.equal(atkHits(h, u).filter((c) => c.dmg.attackId < id).length, 3, `T${tier}: after 3 plain attacks`);
assert.equal(h.hooksOf('heal').filter((c) => c.source === u && c.target === u).length, atkHits(h, u).length, `T${tier}: one 女神的振奋 per strike`);
done(h);
}
});
test('S2 信念的长鞭 (MANUAL, data DEFAULT): 20 / 22 s, range 2-2 + 1 tile forward, ATK +35 / 50 %, each attack 60 / 70 % to stun 0.2 s; all back after', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, INSY]]) {
const sk = skillOf(tier, elite, S2);
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 9 });
// the data's DEFAULT: tools/build-data.mjs ATTACK_RANGE_CHANGE misses "攻击范围向前延伸一格" (reported, not worked around)
assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.duration, sk.bb.atk, sk.bb.ability_range_forward_extend, sk.bb['attack@buff_prob'], sk.bb['attack@stun']],
['DEFAULT', 'DEFAULT', elite ? 22 : 20, elite ? 0.5 : 0.35, 1, elite ? 0.7 : 0.6, 0.2], `T${tier}`);
const far = h.spawn('enemy_dummy', { pos: [10, 8] }); // 3 tiles ahead: only in the extended range
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, `T${tier}: nothing in her own 2-2: no cast (DEFAULT)`);
const near = h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast on her attack`);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.01);
assert.deepEqual(u.liveRangeGrid, [[0, 0], [0, 1], [0, 2], [0, 3]], `T${tier}: one tile more forward`);
const v = talentOf(u, 0)['peak_performance.atk'];
approx(u.s.atk, u.base.atk * (1 + v + sk.bb.atk), `T${tier}: ATK +${sk.bb.atk * 100} %`);
h.b.kill(near, null);
const n0 = atkHits(h, u).length;
u.skill.extend(300);
h.run(150);
const hits = atkHits(h, u).slice(n0);
assert.ok(hits.length > 100 && hits.every((c) => c.target === far), `T${tier}: the far dummy is hit (${hits.length})`);
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun' && c.target === far);
const r = stuns.length / hits.length;
assert.ok(Math.abs(r - sk.bb['attack@buff_prob']) < 0.08, `T${tier}: ${stuns.length} / ${hits.length} stuns ≈ ${sk.bb['attack@buff_prob']}`);
for (const c of stuns) approx(c.duration, 0.2, `T${tier}: stun 0.2 s`);
h.runUntil(() => !u.skill.active, 200);
h.step();
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 2-2`);
approx(u.s.atk, u.base.atk * (1 + v), `T${tier}: ATK back`);
done(h);
}
});
test('S3 英勇的祝福 (MANUAL, data DEFAULT): 30 s, ATK +55 / 70 %, three targets; the operator on the low-ground tile in front holds 精力充沛 +35 / 40 % (> 80 % HP), DEF +20 / 25 %, block +1 — else she does, and it moves on deploy / retreat', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, INSX], [6, true, INSY]]) {
const sk = skillOf(tier, elite, S3);
const b = sk.bb;
assert.deepEqual([sk.duration, b.atk, b['attack@max_target'], b['attack@peak_performance.atk'], b['attack@peak_performance.hp_ratio'], b['attack@def'], b['attack@block_cnt']],
[30, elite ? 0.7 : 0.55, 3, elite ? 0.4 : 0.35, 0.8, elite ? 0.25 : 0.2, 1], `T${tier}`);
const v1 = (u) => talentOf(u, 0)['peak_performance.atk'];
// (a) 角峰 (an operator on a road tile) in front: it holds
{
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 6 }] });
const yak = h.unit(2);
assert.equal(u.skill.rule, 'DEFAULT', `T${tier}: data DEFAULT`);
for (const pos of [[10, 5], [10, 6], [10, 7]]) h.spawn('enemy_dummy', { pos }); // three enemies on her 2-2
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
h.step();
approx(u.skill.timeLeft, 30, `T${tier}: 30 s`, 0.1);
assert.deepEqual(yak.findBuff(holderKey(u))?.mods, { defPct: b['attack@def'], blockCnt: 1 }, `T${tier}: 角峰 holds`);
assert.equal(yak.s.blockCnt, yak.base.blockCnt + 1, `T${tier}: 角峰 block +1`);
approx(yak.s.def, yak.base.def * (1 + b['attack@def']), `T${tier}: 角峰 DEF`);
assert.deepEqual(yak.findBuff(PEAK_KEY)?.mods, { atkPct: b['attack@peak_performance.atk'] }, `T${tier}: 角峰 精力充沛`);
assert.equal(u.findBuff(holderKey(u)), null, `T${tier}: not her`);
assert.equal(u.s.blockCnt, 2);
approx(u.s.atk, u.base.atk * (1 + v1(u) + b.atk), `T${tier}: her ATK +${b.atk * 100} % (+ her own T1)`);
yak.hp = yak.s.maxHp * 0.8;
h.step();
assert.equal(yak.findBuff(PEAK_KEY), null, `T${tier}: 角峰 at 80 % HP: no 精力充沛`);
// three targets per attack
const t0 = h.b.time;
h.run(4);
const per = new Map();
for (const c of atkHits(h, u).filter((x) => x.t > t0)) per.set(c.dmg.attackId, (per.get(c.dmg.attackId) ?? new Set()).add(c.target));
assert.ok(per.size >= 2 && [...per.values()].every((s) => s.size === 3), `T${tier}: three targets per attack`);
// 角峰 leaves ⇒ she holds at once (同类取最高: her 精力充沛 is the higher of T1 and S3); a new operator in front takes it back
h.b.retreat(yak);
h.step();
assert.deepEqual(u.findBuff(holderKey(u))?.mods, { defPct: b['attack@def'], blockCnt: 1 }, `T${tier}: passed to her`);
assert.equal(u.s.blockCnt, 3, `T${tier}: her block +1`);
assert.deepEqual(u.findBuff(PEAK_KEY)?.mods, { atkPct: Math.max(v1(u), b['attack@peak_performance.atk']) }, `T${tier}: one 精力充沛, the highest`);
approx(u.s.atk, u.base.atk * (1 + b.atk + Math.max(v1(u), b['attack@peak_performance.atk'])), `T${tier}: her ATK`);
h.b.redeploy(yak, { free: true });
h.step();
assert.ok(yak.findBuff(holderKey(u)) && !u.findBuff(holderKey(u)), `T${tier}: back to the redeployed 角峰`);
h.runUntil(() => !u.skill.active, 40);
h.step();
assert.ok(!yak.findBuff(holderKey(u)) && !u.findBuff(holderKey(u)) && yak.s.blockCnt === yak.base.blockCnt, `T${tier}: gone at the end`);
assert.deepEqual(u.findBuff(PEAK_KEY)?.mods, { atkPct: v1(u) }, `T${tier}: her T1 only`);
done(h);
}
// (b) the tile in front is high ground (a ranged operator on it): she holds (the empty tile: (a) after the retreat)
{
const rows = { 10: '##hrrrhrrrfrrrrrrrf##' }; // (10,6) = 高台 'h'
const { h, u } = field({ tier, elite, mod, skill: 2, rows, others: [{ uid: 2, chessId: 'chess_char_1_01_a', row: 10, col: 6 }] });
const sniper = h.unit(2);
assert.equal(h.b.grid.isLow(10, 6), false);
h.spawn('enemy_dummy', { pos: [10, 7] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
h.step();
assert.ok(u.findBuff(holderKey(u)) && !sniper.findBuff(holderKey(u)), `T${tier}: an operator on a 高台 in front does not hold it`);
done(h);
}
}
});
test('INS-Y 「可以额外部署在远程位」: the data carries it (hidden buildable_type 2); a placement rule of the prep, not of the battle — a 帕拉斯 piece stays on melee tiles (owner\'s decision of 2026-10-04, shared/highGround.js)', () => {
for (const tier of [5, 6]) {
const m = modOf(tier, INSY);
assert.equal(m.traitOverride.moduleDesc, '可以额外部署在远程位');
assert.deepEqual(m.talentChanges.find((t) => t.talentIndex === -1)?.bb, { buildable_type: 2 });
const rec = diyRecord(SLOT[tier], { charId: PALLAS, skillIndex: 0, uniEquipId: INSY }, { elite: true, data: { chess: CHESS, backups: BACKUPS } });
assert.equal(positionClass(rec, INSY), 'melee', `T${tier}: placement unchanged`);
const { h, u } = field({ tier, elite: true, mod: INSY, skill: 2 });
assert.equal(u.def.position, 'MELEE');
done(h);
}
});
+346 -96
View File
@@ -2427,8 +2427,8 @@
"snaps": "5c632459 14793cab 77a534f8 70d4b39c f84a4105 c2e2b2dc 8596dd2c b7677642 156f5786 2138d829 1cf66223 b3ece3cb 12c6f876 bc62e21b 121a2081 3e5bd85f 09aec046 1e39e676 f98840f6 b64b7602 d2ef96e7 a0415358 32a50205"
},
"diy-060": {
"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=pallas/S1 5_diy2_a=pallas/S2 5_diy1_a=pallas/S3 5_diy2_b=pallas/S1 5_diy1_b=pallas/S2 5_diy2_b=pallas/S3 5_diy1_b=pallas/S1/002_pallas 5_diy2_b=pallas/S2/002_pallas 5_diy1_b=pallas/S3/002_pallas 5_diy2_b=pallas/S1/003_pallas 5_diy1_b=pallas/S2/003_pallas 5_diy2_b=pallas/S3/003_pallas",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m05",
"modeId": "mode_single_hard",
@@ -2438,115 +2438,121 @@
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 2,
"killed": 1,
"total": 60,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"rngDraws": 47,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":2,"total":60,"perfect":false,"leaks":58,"leaksUncounted":0,"coins":0,"dmg":284234,"bossDmg":0,"heal":28284,"deaths":4,"layerGains":{}}},
"players": {"p1":{"killed":1,"total":60,"perfect":false,"leaks":59,"leaksUncounted":0,"coins":0,"dmg":147642,"bossDmg":0,"heal":24778,"deaths":14,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,1557.76,1,75,0,483,4826,0,35,1],
["p1",2,"op","chess_char_5_diy2_b",2,"none",1032.15,1,62,0,1590,4784,0,100,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,76,1,12317,14550,0,86,2],
["p1",4,"op","chess_char_5_diy2_b",2,"none",3334,1,8,0,49643,16298,0,142,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,17773,46,4],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,7,0,0,0,10511,34,4],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,9,0,52535,0,0,133,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,12,1,64757,0,0,140,3]
["p1",1,"op","chess_char_5_diy1_a",0,null,1413,1,82,0,1598,2946,560,17,4],
["p1",2,"op","chess_char_5_diy2_a",1,null,1413,1,83,0,3238,4586,1640,46,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1413,1,84,0,23087,6001,4974,68,1],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,8,0,28783,9750,6853,154,38],
["p1",5,"op","chess_char_5_diy1_b",1,"none",315.92,1,80,0,8231,8698,4320,79,2],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1778,1,81,1,50326,8307,3911,78,3],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_pallas",0,0,76,0,7779,9104,2520,54,12],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_pallas",1928,1,1,0,0,0,0,0,0],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_pallas",1928,1,3,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_pallas",1778,1,5,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_pallas",1778,1,7,0,0,0,0,0,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_pallas",1778,1,9,0,24600,0,0,54,2]
],
"enemies": {"enemy_1045_hammer_2":[12,0,12,13988,58725],"enemy_1061_zomshd":[48,2,41,26470,316109]},
"hooks": [1,76,6900,1226,1226,3411,3411,38165,6,59,32,28,0,37066,274,19,60,53,1,6,0,0,0],
"events": {"atk":1226,"deploy":16,"die":6,"dmg":3411,"heal":80,"leak":53,"skill":60,"spawn":72,"status":505},
"enemies": {"enemy_1045_hammer_2":[12,0,12,14642,20751],"enemy_1061_zomshd":[48,1,44,34750,211491]},
"hooks": [1,84,6900,740,740,3025,3025,29148,15,71,63,62,0,12991,30,39,60,56,1,15,0,0,0],
"events": {"atk":856,"deploy":24,"die":15,"dmg":3025,"heal":633,"leak":56,"skill":125,"spawn":72,"status":71},
"fx": {},
"snaps": "26ba91e2 75f254e3 51d520f1 e694c25e badbf07b a743b9be c4ce9856 69efbbb3 5c32015d e6838b87 7a7f5a11 5c57d175 17f1bb16 b5e3ca66 3f6e8f75 bd69be79 1023702f 86ec7a85 a78af59b 1e14f22a e6f70be1 ee6e389c 726cafa1"
"snaps": "e3dab2f8 66130113 3d00fb84 3aefe588 52a1f3e5 9bbbc6b3 7c0369d3 8cac2b99 53bdf7f0 59a2e5e2 c42047f6 95b0c4b6 626e5cce 8ab58fcc 50fb3fdb f9ed40bc 23cef3c9 f3083896 cb872695 db24bbbe 79925377 c41277c3 9c2c5eac"
},
"diy-061": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
"about": "6_diy1_a=pallas/S1 6_diy2_a=pallas/S2 6_diy1_a=pallas/S3 6_diy2_b=pallas/S1 6_diy1_b=pallas/S2 6_diy2_b=pallas/S3 6_diy1_b=pallas/S1/002_pallas 6_diy2_b=pallas/S2/002_pallas 6_diy1_b=pallas/S3/002_pallas 6_diy2_b=pallas/S1/003_pallas 6_diy1_b=pallas/S2/003_pallas 6_diy2_b=pallas/S3/003_pallas",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m06",
"modeId": "mode_multi_normal",
"round": 12,
"round": 13,
"seed": 3195700727,
"waveId": "act1autochess_h05",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 5,
"total": 90,
"waveId": "act1autochess_h06",
"time": 216.3,
"ticks": 6489,
"reason": "cleared",
"killed": 1,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 197,
"rngDraws": 32,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":5,"total":90,"perfect":false,"leaks":85,"leaksUncounted":0,"coins":0,"dmg":387609,"bossDmg":0,"heal":45545,"deaths":20,"layerGains":{}}},
"players": {"p1":{"killed":1,"total":105,"perfect":false,"leaks":104,"leaksUncounted":0,"coins":0,"dmg":150810,"bossDmg":0,"heal":12615,"deaths":22,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,2604,1,115,0,142,5208,0,10,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,68818,0,0,0,8],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1904,1,113,0,92452,9270,0,31,5],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,95,0,13954,5276,0,22,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",3411,1,114,3,67128,11247,0,50,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1272,1,116,0,2383,2544,0,10,1],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,86,1,24431,3174,0,60,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,72,0,75509,2744,0,21,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,66,1,42792,3498,0,9,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,111,0,0,5782,4538,14,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,105,0,0,32965,41006,40,2]
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,129,0,987,5079,840,20,4],
["p1",2,"op","chess_char_6_diy2_a",1,null,1413,1,7,0,0,0,0,0,0],
["p1",3,"op","chess_char_6_diy1_a",2,null,1413,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,122,0,4382,6814,1480,33,7],
["p1",5,"op","chess_char_6_diy1_b",1,"none",1778,1,130,0,17060,4696,825,31,1],
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,74,0,10862,5221,1400,23,1],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_pallas",0,0,73,0,10776,6016,1520,32,8],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_pallas",0,0,71,0,10228,4216,480,15,1],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_pallas",0,0,77,1,55370,5605,1069,25,1],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_pallas",0,0,92,0,10290,3656,100,11,2],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_pallas",0,0,107,0,7159,6136,2680,29,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_pallas",0,0,115,0,23697,4456,2220,17,1]
],
"enemies": {"enemy_1116_liprr_2":[27,4,23,27292,142911],"enemy_1119_vofsd":[39,1,37,35828,155126],"enemy_1121_lifbos_2":[24,0,24,18588,89572]},
"hooks": [1,116,6900,395,395,859,850,4023,25,109,24,24,0,23823,0,34,90,84,1,25,0,0,9],
"events": {"atk":455,"deploy":26,"die":25,"dmg":850,"fx":155,"heal":89,"leak":84,"skill":48,"spawn":102,"status":148},
"fx": {"aoe":11,"dodge":9,"dp":13,"liberate":16,"slash":104,"spGain":2},
"snaps": "89747c72 f32ee541 745116cd d1ebaa66 351de252 033f3aaf 32429253 1aa1a145 e603afb5 6544f2e7 54e5240e 3b6c73c3 baa30d04 4399886d 2e3a069b fe929f49 170a72f6 5708073c ba5c7dc1 904b04dc e90507fe a4d0c601 3b13368c"
"enemies": {"enemy_1116_liprr_2":[27,1,26,5523,74289],"enemy_1119_vofsd":[48,0,48,21145,47260],"enemy_1121_lifbos_2":[30,0,30,25228,29261]},
"hooks": [1,130,6489,329,329,391,391,3881,23,127,28,28,0,7662,25,44,105,104,1,23,0,0,0],
"events": {"atk":370,"deploy":25,"die":23,"dmg":391,"fx":24,"heal":299,"leak":104,"skill":56,"spawn":117,"status":213},
"fx": {"liberate":24},
"snaps": "b9ffdd79 46a5209b 18a79664 8ff2ad37 069dadda 5c8c9276 607475ef 7e3ae4c3 c725f478 5c2d473e 6104ad6f 6df9ca10 ebdccbc4 76ec3fad 5e8ef0b1 10a31df7 3949096d a5abb8b0 981c2f0a 9799d0a5 d5ae8c29 1ba5b9b5"
},
"diy-062": {
"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=lessng/S1 5_diy2_a=lessng/S2 5_diy1_a=lessng/S3 5_diy2_b=lessng/S1 5_diy1_b=lessng/S2 5_diy2_b=lessng/S3 5_diy1_b=lessng/S1/002_lessng 5_diy2_b=lessng/S2/002_lessng 5_diy1_b=lessng/S3/002_lessng 5_diy2_b=lessng/S1/003_lessng 5_diy1_b=lessng/S2/003_lessng 5_diy2_b=lessng/S3/003_lessng",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_hard",
"round": 13,
"round": 12,
"seed": 1555302922,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 5,
"total": 93,
"waveId": "act1autochess_h05",
"time": 193.03,
"ticks": 5791,
"reason": "cleared",
"killed": 25,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 19,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":5,"total":93,"perfect":false,"leaks":88,"leaksUncounted":0,"coins":0,"dmg":54564,"bossDmg":0,"heal":0,"deaths":32,"layerGains":{}}},
"players": {"p1":{"killed":25,"total":102,"perfect":false,"leaks":77,"leaksUncounted":0,"coins":0,"dmg":229798,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,4,0,0,2604,0,0,0],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,7,0,0,3431,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,80,1,4579,3730,0,14,1],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,11,2,29743,2374,0,16,1],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,115,1,4283,4587,0,0,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1904,1,128,1,8646,3808,0,0,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,129,0,0,10386,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",925.13,1,130,0,0,10659,0,0,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,5,0,0,2638,0,0,0],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,8,0,0,3531,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,77,0,1809,2320,0,5,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,78,0,5505,2860,0,12,1],
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,111,2,8325,5596,0,15,3],
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,94,3,19017,5596,0,21,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,95,4,12587,6772,0,20,1],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,91,1,15250,6768,0,20,4],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,87,2,41746,6768,0,19,0],
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,85,1,12961,6768,0,15,0],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_lessng",0,0,83,0,16797,7228,0,14,2],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_lessng",0,0,121,2,28251,7228,0,26,0],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_lessng",3614,1,125,3,6935,3614,0,17,1],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_lessng",0,0,7,2,16089,4847,0,22,5],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_lessng",0,0,9,3,18919,4724,0,20,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_lessng",0,0,11,2,32920,6339,0,23,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1005_yokai":[33,2,31,0,6238],"enemy_1005_yokai_2":[48,3,43,19613,20845],"enemy_1112_emppnt":[12,0,12,0,27481]},
"hooks": [1,130,6900,249,249,262,283,0,37,123,6,22,0,8025,369,0,93,86,1,37,0,0,1],
"events": {"atk":257,"deploy":35,"die":37,"dmg":283,"fx":104,"leak":86,"skill":48,"spawn":107,"status":70},
"fx": {"bombard":41,"bombardShell":41,"dodge":2,"dp":4,"slash":16},
"snaps": "87cf439f 5dffa1b0 1ce52b5a 26b45bc5 f3e158be a622a441 dfab818e 3fe36df4 ab444c1e 289334db f9131642 eb76ae0d 6be95e92 c5005ec3 867ec509 1c48fcfe d7cd0068 7468c06f be901267 54286eb9 52808145 9724735d 73ac9fbc"
"enemies": {
"enemy_1197_sfshu": [18,4,14,5913,29860],
"enemy_1197_sfshu_2": [27,5,22,10395,50015],
"enemy_1198_msfshu": [12,8,4,0,25],
"enemy_1198_msfshu_2": [15,8,7,0,37],
"enemy_9010_acpupp": [30,0,30,55940,149861]
},
"hooks": [1,125,5791,321,321,357,357,0,45,122,17,24,0,4107,0,29,102,77,1,48,0,0,0],
"events": {"atk":321,"deploy":21,"die":45,"dmg":357,"fx":12,"leak":77,"skill":34,"spawn":116,"status":68},
"fx": {"revive":3,"summon":9},
"snaps": "f09ad148 7b9a0c7b 541c5c05 87e86c5e 2d038f87 4c6a0cab 9d7f5c13 500956b0 629db6af e4432b06 f39bcef1 57dbe0f1 7f7ab902 46d6269a 37bd1038 93e441f7 02d6511f 8f10caa5 b46579ca 6e64c5a2"
},
"diy-063": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
"about": "6_diy1_a=lessng/S1 6_diy2_a=lessng/S2 6_diy1_a=lessng/S3 6_diy2_b=lessng/S1 6_diy1_b=lessng/S2 6_diy2_b=lessng/S3 6_diy1_b=lessng/S1/002_lessng 6_diy2_b=lessng/S2/002_lessng 6_diy1_b=lessng/S3/002_lessng 6_diy2_b=lessng/S1/003_lessng 6_diy1_b=lessng/S2/003_lessng 6_diy2_b=lessng/S3/003_lessng",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
@@ -2557,32 +2563,276 @@
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 10,
"killed": 3,
"total": 78,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1134,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":10,"total":78,"perfect":false,"leaks":68,"leaksUncounted":0,"coins":0,"dmg":618316,"bossDmg":0,"heal":7413,"deaths":27,"layerGains":{}}},
"players": {"p1":{"killed":3,"total":78,"perfect":false,"leaks":75,"leaksUncounted":0,"coins":0,"dmg":243063,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,69,1,10853,3730,0,16,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,66,1,22014,4988,0,21,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,108,0,2037,10590,0,40,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,110,0,11451,9736,0,46,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,2,0,14458,0,0,195,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,3,1,40006,0,0,202,3],
["p1",7,"op","chess_char_6_diy1_a",2,null,1372,1,4,2,310425,0,0,72,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,11,2,151111,7771,0,19,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,8,0,0,1649,7413,11,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,56,0,0,2928,0,0,0],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,99,1,16457,2320,0,26,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",333.17,1,109,2,39504,4017,0,53,1]
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,83,0,9875,5596,0,25,6],
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,70,0,7072,5596,0,24,0],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,71,0,9860,6548,0,25,1],
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,89,0,8938,6768,0,19,4],
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,66,0,19004,6768,0,28,0],
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,96,0,7342,9530,0,26,2],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_lessng",3784,1,100,0,32705,7568,0,32,8],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_lessng",0,0,99,1,38887,7568,0,35,0],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_lessng",0,0,2,1,25688,4980,0,22,1],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_lessng",0,0,6,0,21002,5263,0,25,6],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_lessng",0,0,11,0,20412,5263,0,26,0],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_lessng",0,0,59,1,42278,11777,0,46,2]
],
"enemies": {"enemy_1045_hammer_2":[18,2,16,22272,173200],"enemy_1061_zomshd":[60,8,51,25456,445116]},
"hooks": [1,110,6900,787,787,2114,2147,29564,37,104,14,27,0,18946,1486,38,78,67,1,37,0,0,0],
"events": {"atk":1103,"deploy":32,"die":37,"dmg":2099,"fx":13,"heal":19,"leak":67,"skill":55,"spawn":90,"status":292},
"fx": {"aoe":6,"dodge":4,"spGain":3},
"snaps": "c86567f8 c05b0441 10ec30ba 6ed4f36f 7a0f336f 0db2dc55 27d7fe4f 53d3e729 12ce74d7 7fbab53f 64066d85 48532f28 6780d726 f8a9d91c 1c235846 02daf067 67f907b2 0b4301d3 0ec1b982 a1c78117 2ac034aa 3e951888 742dc3e0"
"enemies": {"enemy_1045_hammer_2":[18,0,18,13022,2254],"enemy_1061_zomshd":[60,3,56,70204,240809]},
"hooks": [1,100,6900,527,527,565,565,18696,24,98,30,37,0,5057,1,25,78,74,1,28,0,0,0],
"events": {"atk":527,"deploy":22,"die":24,"dmg":565,"fx":4,"leak":74,"skill":60,"spawn":90,"status":78},
"fx": {"revive":4},
"snaps": "854a2458 374ded97 ff5c8f75 161eade0 47e2bc02 9e6aeb62 6d2f2dd0 80879eb9 548ac57c b83a5dd4 21ebfa97 78f495f5 1cff6fbf ae29ea53 afd0bf3b 80610759 9295d254 40dc1d49 1e2b699d 7d73f8d0 72992da5 f6b465b8 b27c4a9b"
},
"diy-064": {
"about": "5_diy1_a=demetr/S1 5_diy2_a=demetr/S2 5_diy1_a=demetr/S3 5_diy2_b=demetr/S1 5_diy1_b=demetr/S2 5_diy2_b=demetr/S3 5_diy1_b=demetr/S1/002_demetr 5_diy2_b=demetr/S2/002_demetr 5_diy1_b=demetr/S3/002_demetr 6_diy1_a=demetr/S1 6_diy2_a=demetr/S2 6_diy1_a=demetr/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 2569087536,
"waveId": "act1autochess_h05",
"time": 202.23,
"ticks": 6067,
"reason": "cleared",
"killed": 24,
"total": 153,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 502,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":24,"total":153,"perfect":false,"leaks":129,"leaksUncounted":0,"coins":0,"dmg":957807,"bossDmg":0,"heal":0,"deaths":22,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,133,1,35499,4304,0,63,12],
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,147,2,122440,4304,0,113,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,173,2,60501,4304,0,98,2],
["p1",4,"op","chess_char_5_diy2_b",0,"none",2466,1,199,2,37550,4932,0,57,11],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,115,3,146278,4932,0,98,2],
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,177,3,124760,4932,0,124,2],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_demetr",2606,1,197,1,39795,5212,0,64,12],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_demetr",0,0,121,4,126322,5212,0,73,2],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_demetr",0,0,164,4,98371,5212,0,98,2],
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,156,0,28139,4304,0,50,9],
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,129,0,68354,4304,0,82,3],
["p1",12,"op","chess_char_6_diy1_a",2,null,2152,1,198,2,69799,0,0,119,3],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1249_lysdb_2":[60,10,50,28054,598109],"enemy_1251_lysyta":[63,14,49,9044,281265],"enemy_1329_cbshld":[30,0,30,14853,78434]},
"hooks": [1,199,6067,1292,1292,1745,1578,0,46,175,62,62,0,14235,0,59,153,129,1,52,0,0,129],
"events": {"atk":1701,"deploy":44,"die":46,"dmg":1578,"fx":183,"leak":129,"skill":124,"spawn":167,"status":68},
"fx": {"dodge":129,"shieldBreak":38,"teleport":10,"undying":6},
"snaps": "d7a4c4b8 d1b3d92e 8cf50abd f866abf9 96ede988 8cec9f3d f62404fe 6dc40d5b 799f0eb4 1efd07dc 63b0fb0a 13f97919 2848c1c0 939ec9a5 8033448c 27fbb548 e861cf3a a7167207 6073a250 b79fb995 7957b19c"
},
"diy-065": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 6_diy1_b=demetr/S1 6_diy2_b=demetr/S2 6_diy1_b=demetr/S3 6_diy2_b=demetr/S1/002_demetr 6_diy1_b=demetr/S2/002_demetr 6_diy2_b=demetr/S3/002_demetr 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m03",
"modeId": "mode_multi_funny",
"round": 13,
"seed": 1168351812,
"waveId": "act1autochess_h06",
"time": 218.63,
"ticks": 6559,
"reason": "cleared",
"killed": 62,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 386,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":62,"total":105,"perfect":false,"leaks":43,"leaksUncounted":0,"coins":0,"dmg":939462,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,159,1,7435,4256,0,51,1],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,144,0,8582,5816,0,37,1],
["p1",3,"op","chess_char_6_diy1_b",0,"none",0,0,126,2,30063,4932,0,76,15],
["p1",4,"op","chess_char_6_diy2_b",1,"none",0,0,119,7,150377,4932,0,150,3],
["p1",5,"op","chess_char_6_diy1_b",2,"none",2274,1,161,10,144561,2658,0,192,3],
["p1",6,"op","chess_char_6_diy2_b",0,"uniequip_002_demetr",728,1,157,4,53769,4664,0,80,16],
["p1",7,"op","chess_char_6_diy1_b",1,"uniequip_002_demetr",1520,1,147,9,169639,3872,0,132,4],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_demetr",158.56,1,154,22,297728,2537,0,302,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,130,0,8567,5128,0,28,1],
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,120,1,15950,6668,0,38,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,8,4,17766,0,0,196,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,10,2,35026,0,0,205,3],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null],
["p1","~1","token","token_10065_demetr_dmtpos",null,null,1000,0,27,0,0,0,0,0,null],
["p1","~2","token","token_10065_demetr_dmtpos",null,null,1000,0,43,0,0,0,0,0,null],
["p1","~3","token","token_10065_demetr_dmtpos",null,null,1000,0,96,0,0,0,0,0,null]
],
"enemies": {"enemy_1116_liprr_2":[27,15,12,1609,125049],"enemy_1119_vofsd":[48,30,18,14476,348278],"enemy_1121_lifbos_2":[30,17,13,29379,466135]},
"hooks": [1,161,6559,1729,1729,1988,1911,3731,77,123,55,54,0,25971,0,96,105,43,1,78,0,0,77],
"events": {"atk":1963,"deploy":53,"die":80,"dmg":1911,"fx":131,"leak":43,"skill":109,"spawn":123,"status":209},
"fx": {"dodge":77,"liberate":28,"teleport":25,"undying":1},
"snaps": "d5320ae4 4c9a6086 3ef6da3f 4cecf299 2d9aa2b7 2f24f2b5 2cf4ab07 9299fe0c 7d601db9 48c4c881 c9bcc356 6eb8d1f9 9cfb08bd c28c9da9 794c275f 784648d8 88699e9a 3e2ae987 9e9c9e23 f3a3b1b6 44936fe0 8ed918ed"
},
"diy-066": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_single_hard",
"round": 12,
"seed": 3822982705,
"waveId": "act1autochess_h05",
"time": 222.47,
"ticks": 6674,
"reason": "cleared",
"killed": 14,
"total": 111,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 35,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":14,"total":111,"perfect":false,"leaks":97,"leaksUncounted":0,"coins":0,"dmg":301762,"bossDmg":0,"heal":0,"deaths":12,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,107,0,8089,5208,0,76,1],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",2758.74,1,131,1,24872,3983,0,95,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,132,2,26971,4587,0,20,2],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1436.24,1,133,2,33193,4276,0,28,3],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,106,4,30020,5276,0,46,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,101,1,43129,6822,0,52,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,3,2,23155,0,0,92,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,4,1,49524,0,0,172,4],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,13,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,14,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,5,0,25478,0,0,129,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,11,1,37332,0,0,150,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_1010_demon":[42,4,38,12104,111213],"enemy_1422_lrsldr_2":[45,10,35,3684,141826],"enemy_1425_lrcmra_2":[24,0,24,14364,81163]},
"hooks": [1,133,6674,1177,1177,2246,2235,0,26,123,25,23,0,29082,269,41,111,97,1,26,0,0,11],
"events": {"atk":1177,"deploy":20,"die":26,"dmg":2235,"fx":105,"leak":97,"skill":48,"spawn":125,"status":480},
"fx": {"aoe":45,"dodge":11,"dp":9,"slash":40},
"snaps": "6cc88f86 7cacf7c8 93464d70 85641265 c4d1bd60 4e973db2 5fdca821 ff0dcc37 dd3d2832 929511af 9b1f7ecc a2b7a226 3ef1fb1d 8b9fec74 4670ebe6 692461f5 50bbb267 0832f2f1 892dca09 7eca58af 2ac78dcc 2eb3bcc0 d24fc730"
},
"diy-067": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_multi_normal",
"round": 12,
"seed": 2182386222,
"waveId": "act1autochess_h05",
"time": 217.2,
"ticks": 6516,
"reason": "cleared",
"killed": 27,
"total": 138,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1241,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":27,"total":138,"perfect":false,"leaks":111,"leaksUncounted":0,"coins":0,"dmg":817964,"bossDmg":0,"heal":14211,"deaths":22,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,123,4,31761,4138,0,40,1],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,117,5,75873,4748,0,45,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,173,1,19950,9232,0,59,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,171,0,33594,10136,0,53,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,4,4,53886,0,0,193,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,5,7,82918,0,0,214,3],
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,11,2,234458,0,0,74,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,13,3,266847,0,0,77,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,6,0,0,0,14211,29,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,14,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,172,0,1370,2320,0,11,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",631.9,1,154,1,17308,16031,0,74,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_1166_dusbr_2":[45,19,26,7595,304167],"enemy_1169_duphlx_2":[51,7,44,17547,274083],"enemy_1170_dushld_2":[42,1,41,21463,239714]},
"hooks": [1,173,6516,1011,1011,2931,2995,49,49,160,15,29,0,26241,6389,42,138,111,1,49,0,0,0],
"events": {"atk":1387,"deploy":32,"die":49,"dmg":2916,"fx":77,"heal":38,"leak":111,"skill":62,"spawn":153,"status":695},
"fx": {"aoe":33,"displace":28,"dodge":15,"spGain":1},
"snaps": "efb477c6 8151f389 ea30f9f9 2cdfdb02 f2b50565 be21f6eb 54d9544a caf91403 4ca57e04 6100a790 236a3081 090aa38e 83445fa6 35c0712a b18e29ad 504a7170 f0c6b6f8 dfb90d92 829e3424 7a555ecf 736d5ddf aeb357b7"
},
"diy-068": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_multi_hard",
"round": 13,
"seed": 541523556,
"waveId": "act1autochess_h06",
"time": 216.63,
"ticks": 6499,
"reason": "cleared",
"killed": 5,
"total": 147,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 818,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":5,"total":147,"perfect":false,"leaks":142,"leaksUncounted":0,"coins":0,"dmg":740914,"bossDmg":0,"heal":44350,"deaths":19,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,163,0,1058,7812,0,12,0],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,159,0,6558,8040,0,41,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,169,0,11407,4587,0,27,2],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1904,1,171,2,84672,15092,0,54,4],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,144,1,21244,18132,0,47,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,151,1,85946,19220,0,73,3],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,5,0,22123,0,0,168,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,8,1,95449,0,0,192,3],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,6,0,175652,0,0,77,2],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,9,0,236806,0,0,82,3],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,149,0,0,5352,1680,4,0],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,145,0,0,8121,42670,31,2]
],
"enemies": {"enemy_1166_dusbr_2":[39,1,38,5433,224317],"enemy_1169_duphlx_2":[57,4,53,27713,257242],"enemy_1170_dushld_2":[51,0,51,53211,259356]},
"hooks": [1,171,6499,976,976,2593,2581,68,24,166,24,22,0,29191,0,40,147,142,1,30,0,0,12],
"events": {"atk":1211,"deploy":24,"die":24,"dmg":2581,"fx":134,"heal":64,"leak":142,"skill":46,"spawn":159,"status":159},
"fx": {"aoe":61,"dodge":12,"dp":8,"slash":48,"spGain":5},
"snaps": "781b1dba 9953d89c d62f5539 e2966f30 012de962 b2da7aff a9017dce 20209563 9745406a acd54467 14b08371 eeb3f082 0656caee b76ca44b b907e3c5 1ff44173 549045c2 4a92e0a2 3d45b025 b5c6c9e3 6c636a65 5c0049aa"
},
"diy-069": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_single_normal",
"round": 13,
"seed": 3435515545,
"waveId": "act1autochess_h06",
"time": 218.87,
"ticks": 6566,
"reason": "cleared",
"killed": 4,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 158,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":4,"total":102,"perfect":false,"leaks":98,"leaksUncounted":0,"coins":0,"dmg":279006,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,130,0,19608,5595,0,24,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,127,1,49502,7482,0,36,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,131,0,26763,10386,0,48,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,128,3,69526,11872,0,81,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,6,0,40716,0,0,86,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,9,0,72891,0,0,118,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_1267_nhpbr_2":[27,4,23,7795,94117],"enemy_1270_nhstlk_2":[36,0,36,9450,76170],"enemy_9006_actoxi":[39,0,39,18089,108718]},
"hooks": [1,131,6566,509,509,689,728,0,27,125,6,23,0,11252,4464,32,102,98,1,27,0,0,4],
"events": {"atk":523,"deploy":26,"die":27,"dmg":670,"fx":37,"leak":98,"skill":48,"spawn":111,"status":658},
"fx": {"displace":28,"dodge":5,"zone":4},
"snaps": "d87bacee 4a5b5184 8bd75e7e 15a97a2d 1f5b6cda f88d6647 d127aecc 47aa891f a326d3f1 76566f37 9213718e aadabba7 6e040097 4a6230ed d1f7804a 4f567e2f bda2e258 e2c828a9 a802edca 5544e52a 45da48c6 d8cd6592"
}
}
}