feedback5 O6: 自选 operator kits for 黑, 焰狐龙梓兰 and 鸿雪 (+ her “打字机”) — every skill, talent, trait and module at every form

- op-shwaz.js 黑: 破甲箭头 (one roll per attack, raised by S1 / S2 / S3; ×atk_scale, then defDown on a landed hit; numbers
  from the base talent + ARC-Y's named change — the data folds ARC-Y's hidden trait talent into talent 0 at stage 1),
  交叉火力 (field aura; ARC-X stage 3 "携带": her player's snipers for the battle), ARC-Y 正前方 ×1.05 undodgeable, S3 3-2 +
  interval +0.4; triggers from the data (S3 SKILL_RANGE — reported).
- op-orchd2.js 焰狐龙梓兰: 三连击 normal attack cut to 33.3 % after DEF (PRTS 特性备注), 强击瓶专家 stacks per round, 翔虫机动
  (respawn −15 / −18 s, ATK +15 / 20 % for 30 s on a redeploy at her mark), S1 刚射 / 刚连射 (charges, refund without a
  target, stun per arrow), S2 飞翔瞪射 (free cast at every deployment, 起飞, three volleys on the 4-4, landing on the 1-3,
  timed by the official skeleton), S3 龙之箭 (3 s charge, piercing arrow at 10 tiles/s, strikes every 0.25 tile, push).
- op-bgsnow.js 鸿雪: S1 toggle + proc, S2 点题 (立即, 普通 + 2 × 持续伤害), S3 3-1 with 正前方3格 scale, ARC-Y as 黑's; her
  placed “打字机” pieces get their kit at her install (25 s life, 40 s redeploy × S2's 65 / 60 %, 5 DP; the skill copies;
  弱点速记 DEF cut 18 / 23 %, ARC-X stage 3 28 / 33 % with 八格).
- kits/index.js OPERATOR_KIT_FILES: one line per file. Tests: test/content/op_shwaz / op_orchd2 / op_bgsnow (23).
- Golden: only the diy family moved — diy-003 … diy-006 now hold the 66 new 自选 configurations (黑, 焰狐龙梓兰, 鸿雪) in
  place of the prototype picks, diy-007 … diy-012 are new (the prototype battles moved down); every other family is
  byte-identical. test/golden/README.md: 12 diy battles, 61 of 156.
This commit is contained in:
sganggs committed 2026-10-06 00:10:34 +08:00
1 parent 742f478526
commit 4075338517
9 files changed
+1960 -103

No files matched your search

+306
View File
@@ -0,0 +1,306 @@
// test/content/op_bgsnow.test.js — the 自选 operator kit of 鸿雪 (char_4055_bgsnow, 6★ 重射手; kit
// server/sim/content/kits/ops/op-bgsnow.js) and of her summon “打字机” (token_10026_bgsnow_subbow), fielded the production
// way (a DIY slot + its `diy` pick, simdata getDiy; the typewriter as the placed hand piece of her player) in every form:
// tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, ARC-Y “打字机色带” or
// ARC-X “仪式感” at stage 1 (tier 5) / 3 (tier 6). Numbers from data/backups.json; the fidelity checklist of kits/README.md.
// Run: node --test test/content/op_bgsnow.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const SNOW = 'char_4055_bgsnow';
const TOK = 'token_10026_bgsnow_subbow';
const FORMS = BACKUPS.units[SNOW].forms;
const TOKREC = BACKUPS.tokens[TOK];
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const Y = 'uniequip_002_bgsnow', X = 'uniequip_003_bgsnow';
const S1 = 'skchr_bgsnow_1', S2 = 'skchr_bgsnow_2', S3 = 'skchr_bgsnow_3';
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
/** The typewriter's variant of a form, with the pick's skill / module (getDiyToken). */
const tokOf = (tier, elite, skill, mod) => {
let v = TOKREC.variants[`${SNOW}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
return v;
};
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, Y, X].map((m) => [t, true, m]))];
/** 鸿雪 as uid 1 at (10, 5) facing RIGHT; `tok` = the typewriter piece's tile (uid 2) or null. */
function field({ tier = 5, elite = false, mod = null, skill = 0, tok = null, tokFirst = false, seed = 5, others = [] } = {}) {
const op = { uid: 1, diy: { slot: SLOT[tier], charId: SNOW, skillIndex: skill, uniEquipId: mod }, elite, row: 10, col: 5 };
const piece = tok ? [{ uid: 2, kind: 'token', tokenId: TOK, ownerUid: 1, row: tok[0], col: tok[1] }] : [];
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack', 'dodge'], captureNoisy: true,
units: tokFirst ? [...piece, op, ...others] : [op, ...piece, ...others],
});
h.step();
return { h, u: h.unit(1), t: h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === TOK) ?? null };
}
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
test('鸿雪 in every 自选 form: her kit, the form\'s stats + module attributes, 3-6, ranged physical, hits air, blocks 1, ground-targetable, the data triggers; offered as a pick', () => {
assert.equal(OPERATOR_KITS[SNOW], KITS[SNOW]);
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], [SNOW, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.respawnTime],
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.respawnTime + (m?.attr.respawnTime ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [1, 'ranged', true, 'phys', 1.6], `${label(f)}: 重射手`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-6`);
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'SKILL_RANGE', 'ACTIVE_RANGE']);
assert.deepEqual([modOf(5, Y).attr, modOf(6, Y).attr, modOf(5, X).attr, modOf(6, X).attr],
[{ maxHp: 130, atk: 55 }, { maxHp: 170, atk: 75 }, { atk: 80, def: 10, respawnTime: -25 }, { atk: 120, def: 16, respawnTime: -25 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(SNOW) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(SNOW)));
assert.equal(validateDiyPicks({ [SLOT[5]]: { charId: SNOW, skillIndex: 1, uniEquipId: X } }, { data, kitted: KITTED_CHARS }).ok, true);
});
test('S1 抑扬格 (AUTO, attack SP 29 / 25, data DEFAULT): after that many attacks ATK +35 % / +50 % until she leaves the field, every attack meanwhile 30 % to deal ×145 % / ×160 %', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.spCost, sk.bb.atk, sk.bb.prob, sk.bb.atk_scale], elite ? [25, 0.5, 0.3, 1.6] : [29, 0.35, 0.3, 1.45], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 0, seed: 12 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.spType], ['toggle', 'DEFAULT', 'attack'], `T${tier}`);
h.spawn('enemy_dummy', { pos: [11, 6] });
assert.ok(h.runUntil(() => u.skill.active, 60), `T${tier}: cast`);
const before = from(h, u);
assert.equal(before.length, sk.spCost, `T${tier}: after ${sk.spCost} attacks`);
assert.ok(before.every((c) => Math.abs(c.amount - u.base.atk) < 1e-6), `T${tier}: plain before it`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
h.run(500);
assert.ok(u.skill.active, `T${tier}: 持续时间无限`);
const after = from(h, u).slice(before.length);
const atk = u.base.atk * (1 + sk.bb.atk);
for (const c of after) assert.ok([atk, atk * sk.bb.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: ${c.amount}`);
const r = after.filter((c) => c.amount > atk + 1e-6).length / after.length;
assert.ok(Math.abs(r - sk.bb.prob) < 0.06, `T${tier}: ${r}`);
h.b.retreat(u);
assert.ok(!u.skill.active, `T${tier}: ends when she leaves`);
done(h);
}
});
test('S2 点题 (MANUAL, 2 charges, data SKILL_RANGE on 3-1): an enemy 3 tiles ahead (3-1, not 3-6) casts it and is struck at once: atk_scale × ATK as a 普通伤害 attack, then twice as 持续伤害 (not dodgeable); her card range stays 3-6', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.bb.atk_scale, sk.bb.respawn_time, sk.maxChargeTime, sk.spCost, sk.initSp], elite ? [2, 0.6, 2, 10, 6] : [1.8, 0.65, 2, 11, 3], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 1, seed: 3 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.maxCharges], ['charges', 'SKILL_RANGE', 2], `T${tier}`);
u.skill.gainSp(999);
h.run(1);
assert.equal(u.skill.activations, 0, `T${tier}: nobody in range`);
const e = h.spawn('enemy_dummy', { pos: [10, 8] });
h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), `T${tier}: cast for the enemy on 3-1`);
const tc = h.b.time;
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: the card keeps 3-6`);
h.run(0.5);
const atk = h.hooksOf('attack').filter((c) => c.attacker === u);
approx(atk[0].t, tc, `T${tier}: 立即`, 0.04);
assert.equal(atk[0].isSkill, true);
const dodged = h.hooksOf('dodge').filter((c) => c.source === u);
const hits = from(h, u);
assert.equal(dodged.length, 1, `T${tier}: the 普通伤害 hit can be dodged (a dodge enemy)`);
assert.equal(hits.length, 2, `T${tier}: the two 持续伤害 hits cannot`);
for (const c of hits) {
approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ×${sk.bb.atk_scale}`);
assert.deepEqual([c.dmg.isAttack, c.dmg.canDodge, c.dmg.tags.includes('dot'), c.type], [false, false, true, 'phys']);
}
assert.equal(u.skill.charges, 1, `T${tier}: one of two charges spent`);
h.b.removeBuff(e, 'test:dodge');
const n0 = from(h, u).length;
h.run(3.5);
const second = from(h, u).slice(n0).filter((c) => c.dmg.isSkill);
assert.deepEqual(second.map((c) => [c.dmg.isAttack, c.dmg.tags.includes('dot')]), [[true, false], [false, true], [false, true]], `T${tier}: the second charge 3 s later: 普通 then 2 × 持续`);
done(h);
}
});
test('S3 锐笔速写 (MANUAL, data ACTIVE_RANGE on 3-1): an enemy only on [0,3] casts it; range 3-1 (and back), interval 1.6 − 0.6 s, ×155 % / 170 % off her line, ×185 % / 215 % on the 正前方3格', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.bb.base_attack_time, sk.bb.atk_scale, sk.bb['bgsnow_s_3[atk_up].atk_scale'], sk.rangeId], elite ? [27, -0.6, 1.7, 2.15, '3-1'] : [26, -0.6, 1.55, 1.85, '3-1'], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 2, seed: 7 });
assert.deepEqual([u.skill.kind, u.skill.rule], ['duration', 'ACTIVE_RANGE']);
u.skill.gainSp(999);
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast for [0,3] (outside 3-6)`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-1 while it runs`);
approx(u.s.interval, 1.0, `T${tier}: interval`);
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
h.b.kill(far, null);
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
h.run(8);
const onFront = from(h, u).filter((c) => c.dmg.isAttack && c.target === front);
h.b.kill(front, null);
h.run(4);
const onSide2 = from(h, u).filter((c) => c.dmg.isAttack && c.target === side);
assert.ok(onFront.length > 3 && onSide2.length > 1, `T${tier}: both struck (${onFront.length} / ${onSide2.length})`);
for (const c of onFront) approx(c.amount, u.base.atk * sk.bb['bgsnow_s_3[atk_up].atk_scale'], `T${tier}: 正前方3格`);
for (const c of onSide2) approx(c.amount, u.base.atk * sk.bb.atk_scale, `T${tier}: off the line`);
u.skill.extend(-999);
h.step(2);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-6`);
approx(u.s.interval, 1.6, `T${tier}: interval back`);
done(h);
}
});
test('ARC-Y 打字机色带 trait: ×105 % and no dodge on her straight line ahead (stages 1 and 3) — 点题\'s 持续伤害 hits too; off the line it dodges; ARC-X / no module nothing', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y = elite && mod === Y;
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 3 });
u.skill.charges = 0; u.skill.sp = 0;
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
for (const e of [front, side]) h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
h.run(12);
const onFront = from(h, u).filter((c) => c.target === front);
if (y) {
assert.ok(onFront.length > 4, `${label(f)}: the front enemy is hit`);
for (const c of onFront) approx(c.amount, u.s.atk * 1.05, `${label(f)}: ×1.05`);
// 点题 on the front enemy: all three hits ×1.05
h.b.removeBuff(u, 'test:noSp');
u.skill.addCharge(1);
const n0 = from(h, u).length;
h.run(0.5);
const s2 = from(h, u).slice(n0).filter((c) => c.dmg.isSkill);
assert.equal(s2.length, 3, `${label(f)}: 点题's three hits land`);
for (const c of s2) approx(c.amount, u.s.atk * skillOf(tier, true, S2).bb.atk_scale * 1.05, `${label(f)}: 点题 ×1.05`);
h.b.kill(front, null);
h.run(6);
assert.ok(h.hooksOf('dodge').filter((c) => c.source === u && c.target === side).length > 2, `${label(f)}: off the line it dodges`);
} else {
assert.equal(onFront.length, 0, `${label(f)}: every attack dodged`);
}
done(h);
}
});
test('“打字机”: her placed piece runs its kit (not the generic one) — the variant\'s stats (ARC-Y stage 3 stronger), untargetable, 3-2, blocks 0, hits air; deploys with the board, leaves after 25 s, back on its tile after 40 s (S2: ×65 % / ×60 %) for 5 DP, never while 鸿雪 is down', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const v = tokOf(tier, elite, skill, elite ? mod : null);
const { h, t } = field({ tier, elite, mod, skill, tok: [12, 3], tokFirst: skill === 1 });
assert.ok(t && t.alive && t.deployed, `${label(f)} S${skill + 1}: deployed with the board`);
assert.deepEqual([!!t.kit.generic, t.skill.id, t.base.atk, t.base.maxHp, t.base.respawnTime, t.base.cost], [false, v.skill.skillId, v.stats.atk, v.stats.maxHp, 40, 5], `${label(f)} S${skill + 1}`);
assert.deepEqual([!!t.s.flags.untargetable, t.s.blockCnt, t.profile.canHitFly, t.profile.attack, t.liveRangeGrid], [true, 0, true, 'ranged', [[0, 0], [0, 1], [0, 2], [0, 3]]], `${label(f)} S${skill + 1}: 不会受到攻击`);
done(h);
}
}
assert.deepEqual([tokOf(5, false, 0).stats.atk, tokOf(5, true, 0).stats.atk, tokOf(6, true, 0, Y).stats.atk, tokOf(5, true, 0, Y).stats.atk], [666, 799, 869, 799]);
// life and redeploys (S1: 40 s; S2: ×respawn_time)
for (const [tier, elite, skill, mul] of [[5, false, 0, 1], [5, false, 1, 0.65], [6, true, 1, 0.6]]) {
const { h, u, t } = field({ tier, elite, skill, tok: [12, 3] });
h.b.players[0].dp = 20;
assert.ok(h.runUntil(() => !t.alive, 26));
approx(h.b.time, 25, `T${tier} S${skill + 1}: 25 s`, 0.05);
assert.equal(h.hooksOf('death').find((c) => c.unit === t).reason, 'expired');
assert.ok(h.runUntil(() => t.alive, 60));
approx(h.b.time, 25 + 40 * mul, `T${tier} S${skill + 1}: back after ${40 * mul} s`, 0.3);
assert.deepEqual([t.tileR, t.tileC, h.b.players[0].dp], [12, 3, 15], `T${tier} S${skill + 1}: on its tile, 5 DP`);
assert.ok(u.alive);
done(h);
}
// 鸿雪 down: the typewriter waits for her
const { h, u, t } = field({ tier: 5, elite: false, skill: 0, tok: [12, 3] });
h.b.players[0].dp = 0;
h.run(10);
h.b.kill(u, null);
h.run(60);
assert.ok(!t.alive, 'not while 鸿雪 is down');
h.b.players[0].dp = 25;
assert.ok(h.runUntil(() => u.alive, 30), '鸿雪 back');
assert.ok(h.runUntil(() => t.alive, 1), 'then the typewriter');
done(h);
});
test('“打字机”\'s skill is the copy of hers, free once per deployment: S1 ATK +35 % / +50 % for good with the 30 % proc, S2 three hits of ×180 % / ×200 % (普通 + 2 × 持续), S3 4-1 + interval −0.6 s + ×185 % / ×215 % for 16 / 17 s', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
for (const skill of [0, 1, 2]) {
const v = tokOf(tier, elite, skill, null);
const tb = v.skill.bb;
const { h, t } = field({ tier, elite, skill, tok: [11, 5], seed: 4 });
h.spawn('enemy_dummy', { pos: [11, 7] });
assert.deepEqual([t.skill.rule, t.skill.spCost, t.skill.charges], ['DEFAULT', 0, 1], `T${tier} S${skill + 1}: free, ready`);
assert.ok(h.runUntil(() => t.skill.activations === 1, 1), `T${tier} S${skill + 1}: cast at its first attack`);
if (skill === 0) {
approx(t.s.atk, t.base.atk * (1 + tb.atk), `T${tier}: S1 copy ATK`);
h.run(20);
assert.ok(t.skill.active, `T${tier}: S1 copy stays`);
const hits = from(h, t);
const atk = t.base.atk * (1 + tb.atk);
assert.ok(hits.some((c) => Math.abs(c.amount - atk * tb.atk_scale) < 1e-6) && hits.some((c) => Math.abs(c.amount - atk) < 1e-6), `T${tier}: S1 copy procs ×${tb.atk_scale}`);
} else if (skill === 1) {
h.run(0.5);
const s2 = from(h, t).filter((c) => c.dmg.isSkill);
assert.deepEqual(s2.map((c) => [c.dmg.isAttack, c.dmg.tags.includes('dot')]), [[true, false], [false, true], [false, true]], `T${tier}: S2 copy`);
for (const c of s2) approx(c.amount, t.base.atk * tb.atk_scale, `T${tier}: S2 copy ×${tb.atk_scale}`);
} else {
assert.deepEqual(t.liveRangeGrid, [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]], `T${tier}: S3 copy 4-1`);
approx(t.s.interval, 1.0, `T${tier}: S3 copy interval`);
approx(t.skill.timeLeft, v.skill.duration, `T${tier}: ${v.skill.duration} s`, 0.05);
h.run(3);
for (const c of from(h, t).filter((x) => x.dmg.isSkill)) approx(c.amount, t.base.atk * tb['attack@atk_scale'], `T${tier}: S3 copy ×${tb['attack@atk_scale']}`);
}
h.run(24);
assert.equal(t.skill.activations, 1, `T${tier} S${skill + 1}: once per deployment`);
done(h);
}
}
});
test('弱点速记: every damage of the typewriter cuts the target\'s DEF 18 % for 4 s, 23 % on one of the 4 tiles next to 鸿雪 (not diagonal); ARC-X stage 3: 28 % / 33 % and the 8 tiles; 鸿雪 down ⇒ the base cut', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const x3 = tier === 6 && elite && mod === X;
const tal = tokOf(tier, elite, 2, elite ? mod : null).talents.find((x) => x && x.bb.duration);
const c1 = -tal.bb['bgsnow_token[def_down]_1.def'], c2 = -tal.bb['bgsnow_token[def_down]_2.def'];
assert.deepEqual([c1, c2, tal.bb.duration], x3 ? [0.28, 0.33, 4] : [0.18, 0.23, 4], label(f));
for (const [tile, near] of [[[11, 5], true], [[11, 4], x3], [[12, 3], false]]) {
const { h, u, t } = field({ tier, elite, mod, skill: 0, tok: tile, seed: 2 });
h.spawn('enemy_dummy', { pos: [tile[0], tile[1] + 2] });
h.run(4);
const cuts = h.hooksOf('statusApplied').filter((c) => c.source === t && c.status === 'defDown');
assert.ok(cuts.length >= 2, `${label(f)} ${tile}: cuts`);
for (const c of cuts) assert.deepEqual([c.value, c.duration], [near ? c2 : c1, 4], `${label(f)} ${tile}: ${near ? 'near' : 'apart'}`);
if (near) {
h.b.kill(u, null);
h.run(3);
const after = h.hooksOf('statusApplied').filter((c) => c.source === t && c.status === 'defDown').slice(cuts.length);
assert.ok(after.length && after.every((c) => c.value === c1), `${label(f)}: 鸿雪 down ⇒ ${c1}`);
}
done(h);
}
}
});
+340
View File
@@ -0,0 +1,340 @@
// test/content/op_orchd2.test.js — the 自选 operator kit of 焰狐龙梓兰 (char_1048_orchd2, 6★ 重射手; kit
// server/sim/content/kits/ops/op-orchd2.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or ARC-X
// “梓兰特制箭靶” at stage 1 (tier 5) / 3 (tier 6). Numbers from data/backups.json; the skill clips' moments from the kit's
// CLIP (the official skeleton); the fidelity checklist of kits/README.md item by item.
// Run: node --test test/content/op_orchd2.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool } from '../../shared/diy.js';
import { CLIP, ARROW, NORMAL_HITS } from '../../server/sim/content/kits/ops/op-orchd2.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 ORCHD = 'char_1048_orchd2';
const FORMS = BACKUPS.units[ORCHD].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_orchd2';
const S1 = 'skchr_orchd2_1', S2 = 'skchr_orchd2_2', S3 = 'skchr_orchd2_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);
/** The form's talents with the module's changes (by talentIndex — no hidden trait part here). */
const talentsOf = (tier, elite, mod) => {
const out = formOf(tier, elite).talents.map((t) => ({ ...t, bb: { ...t.bb }, bbStr: { ...t.bbStr } }));
for (const ch of modOf(tier, mod)?.talentChanges ?? []) {
const t = out.find((x) => x.index === ch.talentIndex);
Object.assign(t.bb, ch.bb); Object.assign(t.bbStr, ch.bbStr ?? {});
}
return out;
};
const tBb = (tier, elite, mod, key) => talentsOf(tier, elite, mod).find((t) => t.bb[key] != null).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_def: dummy('enemy_def', { def: 100 }),
enemy_heavy: dummy('enemy_heavy', { mass: 5 }), enemy_gun: dummy('enemy_gun', { atk: 50, range: 2.5, bat: 1 }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, X].map((m) => [t, true, m]))];
const POWER = 'power_attack_count';
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'deploy', 'attack'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: ORCHD, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
const mine = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u);
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'}`;
/** Damage events grouped by attack (attackId). */
const rounds = (list) => { const m = new Map(); for (const c of list) { const k = c.dmg.attackId; if (!m.has(k)) m.set(k, []); m.get(k).push(c); } return [...m.values()]; };
test('焰狐龙梓兰 in every 自选 form: her kit, the form\'s stats + ARC-X, redeploy 70 − 25 (ARC-X) − 15 / 18 (翔虫机动) s, 3-6, a 三连击 sniper that hits air, the data triggers', () => {
assert.equal(OPERATOR_KITS[ORCHD], KITS[ORCHD]);
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], [ORCHD, 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`);
const rt = tBb(tier, elite, mod, 'respawn_time').respawn_time;
assert.equal(u.base.respawnTime, form.stats.respawnTime + (m?.attr.respawnTime ?? 0) + rt, `${label(f)}: redeploy time`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.hits, u.base.bat], [1, 'ranged', true, 'phys', NORMAL_HITS, 1.6], `${label(f)}: 重射手, 三连击`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-6`);
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
// S2 is cast at every deployment: airborne right after the first step
assert.equal(!!u.s.flags.liftoff, skill === 1, `${label(f)}: ground-targetable unless airborne`);
done(h);
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'SKILL_RANGE', 'DEFAULT']);
assert.deepEqual([55, 30, 27], [[5, false, null], [5, true, X], [6, true, X]].map(([t, e, m]) => formOf(t, e).stats.respawnTime + (modOf(t, m)?.attr.respawnTime ?? 0) + tBb(t, e, m, 'respawn_time').respawn_time));
assert.ok(KITTED_CHARS.includes(ORCHD) && [5, 6].every((t) => diyPool(t, { data: { chess: CHESS, backups: BACKUPS }, kitted: KITTED_CHARS }).includes(ORCHD)), 'offered at tiers 5 / 6');
});
test('trait (PRTS 特性备注): a normal attack is 3 hits of 100 % ATK, each ×0.333 after DEF; skill attacks are not cut; air units are hit', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X]]) {
const cut = tBb(tier, elite, mod, 'attack@damage_scale')['attack@damage_scale'];
assert.equal(cut, 0.333);
const { h, u } = field({ tier, elite, mod, skill: 2 });
u.skill.sp = 0;
const e = h.spawn('enemy_def', { pos: [10, 7] });
h.run(20);
const r = rounds(mine(h, u).filter((c) => c.dmg.isAttack && c.target === e));
assert.ok(r.length >= 5, `T${tier}: ${r.length} attacks`);
for (const g of r) {
assert.equal(g.length, NORMAL_HITS, `T${tier}: 三连击`);
for (const c of g) { approx(c.amount, (u.s.atk - 100) * cut, `T${tier}: (ATK − DEF) × 33.3 %`); assert.deepEqual([c.type, c.dmg.isSkill], ['phys', false]); }
}
h.b.kill(e, null);
const f = h.spawn('enemy_fly', { pos: [11, 6] });
h.run(5);
assert.ok(mine(h, u).some((c) => c.target === f), `T${tier}: the flyer is attacked`);
done(h);
}
});
test('S1 刚射 (MANUAL, charges 2 / 3, data DEFAULT): one charge — 4 arrows of 100 % / 120 %; a charge left — it is spent for 刚连射 1.167 s later, 5 arrows of 140 % / 160 %; no normal attack until the clip ends; no target for 刚连射 ⇒ the charge comes back; each 刚连射 arrow 20 % to stun 2 s', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X]]) {
const sk = skillOf(tier, elite, S1);
const pw = tBb(tier, elite, mod, POWER).power_attack_scale;
assert.deepEqual([sk.bb.atk_scale_1, sk.bb.atk_scale_2, sk.bb.stun_prob, sk.bb.stun, sk.maxChargeTime, sk.spCost, sk.initSp],
elite ? [1.2, 1.6, 0.2, 2, 3, 5, 10] : [1, 1.4, 0.2, 2, 2, 7, 7], `T${tier}`);
// (a) one charge: the 4 arrows only, the next normal attack once the clip and the attack interval are over
{
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 17 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.maxCharges, u.skill.charges], ['charges', 'DEFAULT', sk.maxChargeTime, Math.floor(sk.initSp / sk.spCost)], `T${tier}`);
u.skill.charges = 1;
h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 2));
const t0 = h.b.time;
assert.equal(u.skill.charges, 0, `T${tier}: one charge spent`);
h.run(3);
const first = rounds(mine(h, u).filter((c) => c.dmg.isSkill));
assert.deepEqual(first.map((g) => g.length), [4], `T${tier}: one round of 4 arrows`);
for (const c of first[0]) approx(c.amount, u.s.atk * sk.bb.atk_scale_1 * pw, `T${tier}: ×atk_scale_1 (×强击瓶专家)`);
const plain = h.hooksOf('attack').filter((c) => c.attacker === u && !c.isSkill && c.t > t0 + 1e-6);
assert.ok(plain.length && plain[0].t >= t0 + Math.max(CLIP.s1End1 - CLIP.s1Shot, 1.6) - 0.04, `T${tier}: the next normal attack after the clip`);
done(h);
}
// (b) full charges: 刚连射 too, 1.167 s after the first shot, both charges spent, no normal attack inside the 2.0 s clip
{
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 17 });
u.skill.gainSp(999);
const ch0 = u.skill.charges;
h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 2));
const t1 = h.b.time;
assert.equal(u.skill.charges, ch0 - 2, `T${tier}: two charges for 刚射 + 刚连射`);
h.run(3);
const shots = h.hooksOf('attack').filter((c) => c.attacker === u && c.isSkill);
approx(shots[1].t - shots[0].t, CLIP.s1BurstShot - CLIP.s1Shot, `T${tier}: 1.167 s apart`, 0.04);
const sec = rounds(mine(h, u).filter((c) => c.dmg.isSkill));
assert.deepEqual(sec.map((g) => g.length), [4, 5], `T${tier}: 4 then 5 arrows`);
for (const c of sec[1]) approx(c.amount, u.s.atk * sk.bb.atk_scale_2 * pw, `T${tier}: ×atk_scale_2`);
const plain = h.hooksOf('attack').filter((c) => c.attacker === u && !c.isSkill);
assert.ok(plain.length && plain[0].t >= t1 + CLIP.s1End2 - CLIP.s1Shot - 0.04, `T${tier}: no normal attack inside the 2.0 s clip (${plain[0]?.t} vs ${t1})`);
done(h);
}
// (c) its target gone before 刚连射: nothing more, the charge back
{
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 17 });
u.skill.gainSp(999);
const ch0 = u.skill.charges;
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 2));
h.b.kill(e, null);
h.run(1.5);
assert.equal(u.skill.charges, ch0 - 1, `T${tier}: the second charge is back`);
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u && c.isSkill).length, 1, `T${tier}: no 刚连射`);
assert.equal(u.findBuff('orchd2:lock'), null, `T${tier}: the skill stopped (no clip left)`);
done(h);
}
}
// (d) the stun: every 刚连射 arrow rolls stun_prob for `stun` s
const { h, u } = field({ tier: 6, elite: true, mod: X, skill: 0, seed: 9 });
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
for (let i = 0; i < 60; i++) {
u.skill.gainSp(999);
const n = u.skill.activations;
h.runUntil(() => u.skill.activations > n, 5);
h.run(3.5);
}
const arrows = mine(h, u).filter((c) => c.dmg.tags?.includes('orchd2:burst'));
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun' && c.target === e);
assert.equal(arrows.length, 60 * 5);
assert.ok(Math.abs(stuns.length / arrows.length - 0.2) < 0.06, `stun rate ${stuns.length} / ${arrows.length}`);
for (const c of stuns) assert.equal(c.duration, 2);
done(h);
});
test('S2 飞翔瞪射: cast free at every deployment (SP untouched); 起飞 (no ground enemy blocks / hits her) and no normal attack; volleys of 3 / 4 / 5 arrows ×135 % / 150 % at every target of the 4-4 (air too), landing ×255 % / 270 % on the 1-3; the first cast 0.833 s longer; then the data\'s SKILL_RANGE', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X]]) {
const sk = skillOf(tier, elite, S2);
const pw = tBb(tier, elite, mod, POWER).power_attack_scale;
assert.deepEqual([sk.bb['attack@atk_scale_loop'], sk.bb['attack@atk_scale_end'], sk.duration, sk.maxChargeTime, sk.initSp, sk.spCost, sk.rangeId],
elite ? [1.5, 2.7, 4.2, 3, 14, 20, '4-4'] : [1.35, 2.55, 4.2, 2, 11, 20, '4-4'], `T${tier}`);
const h = makeBattle({ defs: { enemies: ENEMIES }, timeLimit: 300, autoFinish: false, seed: 6, flags: { dpPerSec: 0, dpMax: 999 },
hooks: ['damaged', 'skillStart', 'skillEnd', 'attack'], captureNoisy: true,
enemies: [{ key: 'enemy_dummy', pos: [10, 7] }, { key: 'enemy_fly', pos: [11, 9] }, { key: 'enemy_dummy', pos: [9, 6] }, { key: 'enemy_gun', pos: [11, 5] }],
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: ORCHD, skillIndex: 1, uniEquipId: mod }, elite, row: 10, col: 5 }] });
h.step();
const u = h.unit(1);
const [front, fly, near, gun] = h.b.enemies;
assert.deepEqual([u.skill.active, h.hooksOf('skillStart')[0]?.reason, u.skill.charges, u.skill.sp], [true, 'deploy', 0, sk.initSp], `T${tier}: the free deploy cast`);
assert.ok(u.s.flags.liftoff && u.s.flags.blockFly, `T${tier}: airborne`);
const lead = CLIP.s2BeginFirst; // the first cast of the deployment
const landAt = lead + 3 * CLIP.s2Loop + CLIP.s2EndShot;
h.run(landAt + 0.3);
const shots = h.hooksOf('attack').filter((c) => c.attacker === u);
assert.equal(shots.length, 4, `T${tier}: three volleys and the landing — no normal attack`);
shots.slice(0, 3).forEach((c, i) => approx(c.t, lead + i * CLIP.s2Loop + CLIP.s2LoopShot, `T${tier}: volley ${i + 1}`, 0.04));
approx(shots[3].t, landAt, `T${tier}: landing`, 0.04);
const dmg = mine(h, u);
const vol = rounds(dmg.filter((c) => c.dmg.attackId !== dmg.at(-1).dmg.attackId));
assert.deepEqual(vol.map((g) => g.length), [6, 8, 10], `T${tier}: 3 / 4 / 5 arrows at each of the two 4-4 targets`);
for (const g of vol) {
assert.deepEqual([...new Set(g.map((c) => c.target))].sort((a, b) => a.id - b.id), [front, fly].sort((a, b) => a.id - b.id), `T${tier}: the 4-4 targets (the flyer too)`);
for (const c of g) approx(c.amount, u.base.atk * sk.bb['attack@atk_scale_loop'] * pw, `T${tier}: ×loop`);
}
const land = dmg.filter((c) => c.dmg.attackId === dmg.at(-1).dmg.attackId);
assert.deepEqual(land.map((c) => c.target), [near], `T${tier}: the landing hits the 1-3 only`);
approx(land[0].amount, u.base.atk * sk.bb['attack@atk_scale_end'] * pw, `T${tier}: ×end`);
assert.ok(!u.s.flags.liftoff, `T${tier}: landed`);
assert.equal(h.hooksOf('damaged').filter((c) => c.source === gun && c.target === u && c.t < landAt - 1e-6).length, 0, `T${tier}: the ground shooter could not hit her airborne`);
assert.ok(h.runUntil(() => !u.skill.active, 2));
approx(h.hooksOf('skillEnd')[0].t, sk.duration + (CLIP.s2BeginFirst - CLIP.s2Begin), `T${tier}: 4.2 + 0.833 s`, 0.05);
h.run(3);
assert.ok(h.hooksOf('damaged').some((c) => c.source === gun && c.target === u), `T${tier}: on the ground she is hit`);
// later casts: SKILL_RANGE on 4-4 (the 1-3 neighbour alone does not cast it), the 0.667 s start
for (const e of [front, fly]) h.b.kill(e, null);
u.skill.charges = 1;
u.skill.sp = 0;
h.run(4);
assert.equal(u.skill.activations, 1, `T${tier}: nobody on the 4-4, no cast`);
h.spawn('enemy_dummy', { pos: [9, 8] });
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: an enemy on the 4-4 casts it`);
const tc = h.b.time;
assert.equal(h.hooksOf('skillStart').at(-1).reason, 'SKILL_RANGE');
h.runUntil(() => !u.skill.active, 6);
approx(h.b.time - tc, sk.duration, `T${tier}: 4.2 s`, 0.06);
const v2 = h.hooksOf('attack').filter((c) => c.attacker === u && c.t >= tc - 1e-6);
approx(v2[0].t - tc, CLIP.s2Begin + CLIP.s2LoopShot, `T${tier}: first volley 0.833 s in`, 0.04);
done(h);
}
});
test('S3 龙之箭: the arrow leaves 3.1 s after the cast (3.767 s for the first cast), flies straight ahead out of the field, hits every enemy on its way (air too, beyond her range) for ×240 % / 300 % physical then ×30 % / 40 % arts each 0.25 tile, pushes the light ones (中等力度) — not the off-line ones; no normal attack until its clip ends; her range stays', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X]]) {
const sk = skillOf(tier, elite, S3);
const pw = tBb(tier, elite, mod, POWER).power_attack_scale;
assert.deepEqual([sk.bb.atk_scale, sk.bb.atk_scale_magic, sk.bb.wait_duration, sk.bb.dist_interval, sk.bb.force, sk.bb.knockback_duration, sk.maxChargeTime],
elite ? [3, 0.4, 1.5, 0.25, 1, 1, 2] : [2.4, 0.3, 1.5, 0.25, 1, 1, 1], `T${tier}`);
const { h, u } = field({ tier, elite, mod, skill: 2, seed: 2, col: 2 });
assert.deepEqual([u.skill.kind, u.skill.rule], [elite ? 'charges' : 'instant', 'DEFAULT']);
const light = h.spawn('enemy_dummy', { pos: [10, 3] });
const heavy = h.spawn('enemy_heavy', { pos: [10, 6] }); // outside her 3-6 range
const fly = h.spawn('enemy_fly', { pos: [10, 8] });
const off = h.spawn('enemy_dummy', { pos: [11, 6] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 2));
const tc = h.b.time, n0 = mine(h, u).length;
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: range unchanged`);
const shot = tc + CLIP.s3BeginFirst + sk.bb.wait_duration + CLIP.s3EndShot;
h.run(shot - tc + 2);
const arrow = mine(h, u).slice(n0).filter((c) => c.dmg.tags?.includes('orchd2:arrow'));
approx(Math.min(...arrow.map((c) => c.t)), shot + 1 / 30, `T${tier}: shot at +3.767 s (first cast; the tile next to her is reached the tick after)`, 0.04);
for (const e of [light, heavy, fly]) {
const on = arrow.filter((c) => c.target === e);
const ph = on.filter((c) => c.type === 'phys'), ar = on.filter((c) => c.type === 'arts');
assert.ok(ph.length >= 4 && ph.length === ar.length, `T${tier}: ${e.defId}: ${ph.length} strikes`);
for (const c of ph) approx(c.amount, u.base.atk * sk.bb.atk_scale * pw, `T${tier}: physical`);
for (const c of ar) approx(c.amount, u.base.atk * sk.bb.atk_scale_magic * pw, `T${tier}: arts`);
}
assert.equal(arrow.filter((c) => c.target === off).length, 0, `T${tier}: off the line, untouched`);
approx(light.x, 3 + 2.14, `T${tier}: the light one pushed 2.14 tiles (中力 − weight 0)`, 0.02);
assert.equal(heavy.x, 6, `T${tier}: weight 5 does not move`);
const plain = h.hooksOf('attack').filter((c) => c.attacker === u && c.t > tc + 1e-6);
assert.ok(plain.every((c) => c.t >= tc + CLIP.s3BeginFirst + sk.bb.wait_duration + CLIP.s3End - 0.04), `T${tier}: no normal attack inside the clip`);
assert.ok(!u.skill.active, `T${tier}: the clip is over`);
// a later cast: 3.1 s
for (const e of [heavy, fly, off]) h.b.kill(e, null);
h.b.relocate?.(light, 10, 3);
light.x = 3; light.y = 10;
u.skill.gainSp(999);
const a0 = u.skill.activations;
assert.ok(h.runUntil(() => u.skill.activations > a0, 5));
const t2 = h.b.time, n1 = mine(h, u).length;
h.run(4.5);
const later = mine(h, u).slice(n1).filter((c) => c.dmg.tags?.includes('orchd2:arrow'));
approx(later[0].t - t2, CLIP.s3Begin + sk.bb.wait_duration + CLIP.s3EndShot + 1 / 30, `T${tier}: shot at +3.1 s`, 0.04);
done(h);
}
assert.deepEqual([ARROW.speed, ARROW.radius], [10, 0.5]);
});
test('T1 强击瓶专家: from the first cast of a deployment the next 50 attack rounds ×115 % (a 三连击 is one round), none before, none after; a redeploy starts over', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X]]) {
const pb = tBb(tier, elite, mod, POWER);
assert.deepEqual([pb.power_attack_count, pb.power_attack_scale], [50, 1.15]);
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 1 });
const cut = tBb(tier, elite, mod, 'attack@damage_scale')['attack@damage_scale'];
u.skill.charges = 0; u.skill.sp = 0;
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
h.run(8);
const before = rounds(mine(h, u));
assert.ok(before.length >= 4 && before.every((g) => g.every((c) => Math.abs(c.amount - u.s.atk * cut) < 1e-6)), `T${tier}: no bonus before a cast`);
u.skill.addCharge(1);
assert.ok(h.runUntil(() => u.skill.activations === 1, 3));
const n0 = mine(h, u).length;
h.run(1.6 * 60);
const after = rounds(mine(h, u).slice(n0));
const boosted = after.filter((g) => g[0].amount > (g[0].dmg.isSkill ? u.s.atk * skillOf(tier, elite, S1).bb.atk_scale_1 : u.s.atk * cut) * 1.01);
assert.equal(boosted.length, 50, `T${tier}: 50 rounds`);
assert.ok(after.indexOf(boosted.at(-1)) === 49 && after.length > 51, `T${tier}: the first 50, then none`);
assert.equal(u.mem.orchdPower, 0);
h.b.retreat(u);
h.b.redeploy(u);
assert.equal(u.mem.orchdPower, 0, `T${tier}: a redeploy starts over`);
assert.ok(e.alive);
done(h);
}
});
test('T2 翔虫机动: knocked out, she comes back after 70 − 15 s (ARC-X: −25 −15 / −18) on her tile with ATK +15 % (ARC-X stage 3: +20 %) for 30 s; never at the first deployment', () => {
for (const f of [[5, false, null], [5, true, X], [6, true, X]]) {
const [tier, elite, mod] = f;
const wb = tBb(tier, elite, mod, 'atk_duration');
assert.deepEqual([wb.atk, wb.atk_duration], [tier === 6 && mod === X ? 0.2 : 0.15, 30], label(f));
const { h, u } = field({ tier, elite, mod, skill: 2 });
assert.equal(u.findBuff('talent:orchd2:wirebug'), null, `${label(f)}: not at the first deployment`);
h.b.players[0].dp = 999;
const at = [u.tileR, u.tileC];
h.b.kill(u, null);
approx(u.respawnAt - u.deathAt, u.base.respawnTime, label(f));
assert.ok(h.runUntil(() => u.alive, u.base.respawnTime + 1), `${label(f)}: back`);
approx(h.b.time - u.deathAt, u.base.respawnTime, `${label(f)}: after ${u.base.respawnTime} s`, 0.05);
assert.deepEqual([u.tileR, u.tileC], at, `${label(f)}: on her tile`);
const b = u.findBuff('talent:orchd2:wirebug');
assert.deepEqual(b?.mods, { atkPct: wb.atk }, label(f));
approx(b.timeLeft, wb.atk_duration, label(f), 0.01);
approx(u.s.atk, u.base.atk * (1 + wb.atk), label(f));
h.run(31);
assert.equal(u.findBuff('talent:orchd2:wirebug'), null, `${label(f)}: gone after 30 s`);
done(h);
}
});
+271
View File
@@ -0,0 +1,271 @@
// test/content/op_shwaz.test.js — the 自选 operator kit of 黑 (char_340_shwaz, 6★ 重射手; kit
// server/sim/content/kits/ops/op-shwaz.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, ARC-X
// “定制弩配件套装” or ARC-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_shwaz.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const SHWAZ = 'char_340_shwaz';
const FORMS = BACKUPS.units[SHWAZ].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_shwaz', Y = 'uniequip_003_shwaz';
const S1 = 'skchr_shwaz_1', S2 = 'skchr_shwaz_2', S3 = 'skchr_shwaz_3';
const PROVE = 'chess_char_1_07_a', YAK = 'chess_char_1_02_a';
/** 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);
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' }) };
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, X, Y].map((m) => [t, true, m]))];
/** 破甲箭头 of a form: the base talent, upgraded by ARC-Y's NAMED change only (stage 3: 170 % / −25 %). */
const pierce = (tier, elite, mod) => ({ ...formOf(tier, elite).talents[0].bb, ...(modOf(tier, mod)?.talentChanges ?? []).filter((t) => t.talentIndex === 0 && t.name).reduce((o, t) => ({ ...o, ...t.bb }), {}) });
/** 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, players = null } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'dodge'], captureNoisy: true,
...(players ? { players } : { units: [{ uid: 1, diy: { slot: SLOT[tier], charId: SHWAZ, 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'}`;
test('黑 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 3-6, ranged physical, hits air, blocks 1, ground-targetable, data triggers', () => {
assert.equal(OPERATOR_KITS[SHWAZ], KITS[SHWAZ]);
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], [SHWAZ, 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`);
// ARC-X “定制弩配件套装”: "再部署时间减少" — the attribute respawn_time −25 (70 → 45 s)
assert.equal(u.base.respawnTime, form.stats.respawnTime + (m?.attr.respawnTime ?? 0), `${label(f)}: redeploy time`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [1, 'ranged', true, 'phys', 1.6], `${label(f)}: 重射手`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-6`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
done(h);
}
}
// the data: S1 / S2 DEFAULT, S3 SKILL_RANGE on 3-2 (the open question of the kit header); E2 Lv1 1393 / 646 / 164, Lv60 1685 / 775 / 204
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'SKILL_RANGE']);
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [646, 775]);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr],
[{ maxHp: 130, atk: 55, respawnTime: -25 }, { maxHp: 170, atk: 75, respawnTime: -25 }, { atk: 80, def: 10 }, { atk: 120, def: 16 }]);
});
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(SHWAZ));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(SHWAZ), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: SHWAZ, skillIndex: 2, uniEquipId: Y } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: SHWAZ, skillIndex: 2, uniEquipId: Y } } });
});
test('S1 强弩 (AUTO, attack SP 4, data DEFAULT): every 5th attack ×175 % / 190 % ATK, 破甲箭头 then at 60 % / 70 % (20 % otherwise); air units are hit', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1), t0 = pierce(tier, elite, null);
const { h, u } = field({ tier, elite, skill: 0, seed: 21 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.spCost, u.skill.spType, sk.bb.atk_scale, sk.bb['talent@prob']],
['instant', 'DEFAULT', 4, 'attack', elite ? 1.9 : 1.75, elite ? 0.7 : 0.6], `T${tier}`);
h.spawn('enemy_fly', { pos: [10, 6] }); // a flyer in front: she hits air units
h.run(800);
const hits = atkHits(h, u);
const sk1 = hits.filter((c) => c.dmg.isSkill), plain = hits.filter((c) => !c.dmg.isSkill);
assert.ok(Math.abs(sk1.length * 5 - hits.length) <= 5, `T${tier}: one S1 attack in 5 (${sk1.length} / ${hits.length})`);
const base = u.s.atk;
for (const c of sk1) assert.ok([base * sk.bb.atk_scale, base * sk.bb.atk_scale * t0.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: S1 hit ${c.amount}`);
for (const c of plain) assert.ok([base, base * t0.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: plain hit ${c.amount}`);
const rate = (list, scale) => list.filter((c) => c.amount > base * scale + 1e-6).length / list.length;
const r1 = rate(sk1, sk.bb.atk_scale), r0 = rate(plain, 1);
assert.ok(Math.abs(r1 - sk.bb['talent@prob']) < 0.12, `T${tier}: 破甲箭头 on S1 ${r1}`);
assert.ok(Math.abs(r0 - t0.prob) < 0.07, `T${tier}: 破甲箭头 otherwise ${r0}`);
assert.ok(hits.every((c) => c.type === 'phys' && c.target.motion === 'FLY'), `T${tier}: physical, on the flyer`);
done(h);
}
});
test('S2 暮眼锐瞳 (MANUAL, data DEFAULT): ATK +85 % / +100 % for 33 / 36 s, 破甲箭头 at 40 % / 45 % meanwhile; no cast without an enemy', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2), t0 = pierce(tier, elite, null);
const { h, u } = field({ tier, elite, skill: 1, seed: 8 });
assert.deepEqual([u.skill.kind, u.skill.rule, sk.duration, sk.bb.atk, sk.bb['talent@prob']], ['duration', 'DEFAULT', elite ? 36 : 33, elite ? 1 : 0.85, elite ? 0.45 : 0.4], `T${tier}`);
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, `T${tier}: no enemy, no cast`);
h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in range`);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.01);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK +${sk.bb.atk * 100} %`);
const n0 = atkHits(h, u).length;
u.skill.extend(600);
h.runUntil(() => !u.skill.active, 700);
const during = atkHits(h, u).slice(n0);
const atk = u.base.atk * (1 + sk.bb.atk);
assert.ok(during.length > 300 && during.every((c) => c.dmg.isSkill), `T${tier}: ${during.length} attacks, marked`);
const r = during.filter((c) => c.amount > atk + 1e-6).length / during.length;
assert.ok(Math.abs(r - sk.bb['talent@prob']) < 0.07, `T${tier}: 破甲箭头 ${r}`);
for (const c of during.slice(0, 20)) assert.ok([atk, atk * t0.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: ${c.amount}`);
approx(u.s.atk, u.base.atk, `T${tier}: ATK back`);
done(h);
}
});
test('S3 战术的终结: range 3-2 while it runs (and back), interval 1.6 + 0.4 s, ATK +120 % / +140 %, 破甲箭头 on every attack; the data\'s SKILL_RANGE: an enemy on 3-2 casts it, one only on the rest of 3-6 does not', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3), t0 = pierce(tier, elite, null);
assert.deepEqual(sk.rangeGrid, [[0, 0], [0, 1], [0, 2], [0, 3]]);
const { h, u } = field({ tier, elite, skill: 2, seed: 4 });
assert.deepEqual([u.skill.kind, u.skill.rule, sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.bb['talent@prob']],
['duration', 'SKILL_RANGE', elite ? 21 : 18, elite ? 1.4 : 1.2, 0.4, 1], `T${tier}`);
u.skill.gainSp(999);
const side = h.spawn('enemy_dummy', { pos: [11, 6] }); // in 3-6, not in 3-2
h.run(3);
assert.equal(u.skill.activations, 0, `T${tier}: the side tile does not cast it`);
assert.ok(atkHits(h, u).some((c) => c.target === side), `T${tier}: she attacks it (3-6)`);
const far = h.spawn('enemy_dummy', { pos: [10, 8] }); // 3 tiles ahead: 3-2, outside 3-6
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast for the enemy on 3-2`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: range 3-2 while it runs`);
approx(u.s.interval, 2.0, `T${tier}: interval +0.4 s`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
const n0 = atkHits(h, u).length;
h.runUntil(() => !u.skill.active, 30);
const during = atkHits(h, u).slice(n0);
assert.ok(during.length >= 8 && during.every((c) => c.target === far), `T${tier}: only the 3-2 enemy (${during.length})`);
for (const c of during) approx(c.amount, u.base.atk * (1 + sk.bb.atk) * t0.atk_scale, `T${tier}: every attack procs`);
const cuts = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'defDown' && c.target === far);
assert.ok(cuts.length >= during.length - 1, `T${tier}: DEF cut each time`);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-6`);
approx(u.s.interval, 1.6, `T${tier}: interval back`);
done(h);
}
});
test('T1 破甲箭头: a proc ×160 % (ARC-Y stage 3: 170 %) and, once it hit, DEF ×0.8 (×0.75) for 5 s (defDown); ARC-Y stage 1 keeps 160 % (its hidden trait part is not 破甲箭头); a dodged attack cuts nothing', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t0 = pierce(tier, elite, mod);
assert.deepEqual([t0.atk_scale, t0.def, t0.prob, t0.defdown_duration], tier === 6 && mod === Y ? [1.7, -0.25, 0.2, 5] : [1.6, -0.2, 0.2, 5], label(f));
const { h, u } = field({ tier, elite, mod, skill: 2, seed: 13 });
const e = h.spawn('enemy_dummy', { pos: [11, 6] }); // off her front line: no ARC-Y bonus
h.run(120);
const hits = atkHits(h, u).filter((c) => c.target === e);
const procs = hits.filter((c) => c.amount > u.s.atk + 1e-6);
assert.ok(procs.length > 5 && procs.length < hits.length * 0.4, `${label(f)}: ${procs.length} / ${hits.length}`);
for (const c of procs) approx(c.amount, u.s.atk * t0.atk_scale, `${label(f)}: ×${t0.atk_scale}`);
const cuts = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'defDown');
assert.equal(cuts.length, procs.length, `${label(f)}: one DEF cut per proc`);
for (const c of cuts) assert.deepEqual([c.value, c.duration], [-t0.def, t0.defdown_duration], label(f));
// a dodged attack: no hit, no cut
const d = h.spawn('enemy_dummy', { pos: [9, 6] });
h.b.addBuff(d, { key: 'test:dodge', mods: { dodgePhys: 1 } });
h.b.kill(e, null);
const c0 = cuts.length;
h.run(60);
assert.ok(h.hooksOf('dodge').filter((c) => c.source === u).length > 20, `${label(f)}: dodged`);
assert.equal(h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'defDown').length, c0, `${label(f)}: nothing cut`);
done(h);
}
});
test('T2 交叉火力: with another 【狙击】 operator on the field every sniper (her too, a partner\'s too) ATK +8 % — not 角峰, not alone, not once she leaves; ARC-X stage 3 "携带": +13 % on her player\'s snipers for the whole battle', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const squad = tier === 6 && mod === X;
const v = squad ? 0.13 : 0.08;
const t1 = { ...formOf(tier, elite).talents[1].bb, ...(modOf(tier, mod)?.talentChanges ?? []).filter((t) => t.talentIndex === 1).reduce((o, t) => ({ ...o, ...t.bb }), {}) };
assert.equal(t1.atk, v, label(f));
// alone: nothing
const solo = field({ tier, elite, mod, skill: 0 });
solo.h.run(1);
assert.equal(solo.u.findBuff('talent:shwaz:crossfire'), null, `${label(f)}: alone`);
done(solo.h);
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 2, chessId: PROVE, row: 12, col: 3 }, { uid: 3, chessId: YAK, row: 9, col: 3 }] });
h.run(1);
const prove = h.unit(2), yak = h.unit(3);
for (const a of [u, prove]) assert.deepEqual(a.findBuff('talent:shwaz:crossfire')?.mods, { atkPct: v }, `${label(f)}: ${a.def.name}`);
assert.equal(yak.findBuff('talent:shwaz:crossfire'), null, `${label(f)}: 角峰 (重装)`);
approx(u.s.atk, u.base.atk * (1 + v), `${label(f)}: her ATK`);
h.b.retreat(prove);
h.run(1);
assert.equal(!!u.findBuff('talent:shwaz:crossfire'), squad, `${label(f)}: 普罗旺斯 off the field (携带: still in the squad)`);
h.b.redeploy(prove);
h.run(1);
h.b.retreat(u);
h.run(1);
assert.equal(!!prove.findBuff('talent:shwaz:crossfire'), squad, `${label(f)}: 黑 off the field`);
done(h);
}
// a partner's sniper on a shared field: the field version counts it and buffs it; the squad version does not
const pair = (mod, tier) => field({ players: [
{ playerId: 'p1', seat: 0, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: tier === 6 ? 'chess_char_6_diy1_b' : 'chess_char_5_diy1_b', diy: { charId: SHWAZ, skillIndex: 0, uniEquipId: mod }, row: 10, col: 5 }], bonds: {}, bandId: null, playerEffects: [] },
{ playerId: 'p2', seat: 1, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: PROVE, row: 12, col: 3 }], bonds: {}, bandId: null, playerEffects: [] },
] });
for (const [mod, tier, want] of [[null, 6, 0.08], [X, 6, null]]) {
const { h } = pair(mod, tier);
h.run(1);
const mine = h.b.allyUnits.find((a) => a.ownerId === 'p1'), theirs = h.b.allyUnits.find((a) => a.ownerId === 'p2');
assert.equal(mine.findBuff('talent:shwaz:crossfire')?.mods.atkPct ?? null, want, `${mod ?? 'none'}: 黑`);
assert.equal(theirs.findBuff('talent:shwaz:crossfire')?.mods.atkPct ?? null, want, `${mod ?? 'none'}: the partner's 普罗旺斯`);
done(h);
}
});
test('ARC-Y 老剃刀 trait: an enemy on her straight line ahead takes ×105 % and cannot dodge (stages 1 and 3); off the line it dodges; ARC-X / no module change nothing; facing UP turns the line', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y = elite && mod === Y;
if (y) assert.deepEqual(formOf(tier, true).modules.find((m) => m.uniEquipId === Y).traitOverride.bb, { atk_scale: 1.05 }, label(f));
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 3 });
u.skill.sp = 0; u.skill.charges = 0;
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
for (const e of [front, side]) h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
h.run(40);
const onFront = atkHits(h, u).filter((c) => c.target === front);
const dodged = h.hooksOf('dodge').filter((c) => c.source === u);
if (y) {
assert.ok(onFront.length > 10, `${label(f)}: the front enemy is hit (${onFront.length})`);
for (const c of onFront.filter((x) => !x.dmg.isSkill)) assert.ok([u.s.atk * 1.05, u.s.atk * 1.05 * pierce(tier, elite, mod).atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `${label(f)}: ×1.05 (${c.amount})`);
h.b.kill(front, null);
h.run(10);
assert.ok(h.hooksOf('dodge').filter((c) => c.source === u && c.target === side).length > 3, `${label(f)}: off the line it dodges`);
} else {
assert.equal(onFront.length, 0, `${label(f)}: no bonus — every attack dodged`);
assert.ok(dodged.length > 10, label(f));
}
done(h);
}
// facing UP: the line is the column above her
const h = makeBattle({ defs: { enemies: ENEMIES }, timeLimit: 60, autoFinish: false, seed: 2, flags: { dpPerSec: 0 }, hooks: ['damaged'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: 6, charId: SHWAZ, skillIndex: 0, uniEquipId: Y }, elite: true, row: 10, col: 5, dir: 'UP' }] });
h.step();
const u = h.unit(1);
const e = h.spawn('enemy_dummy', { pos: [12, 5] });
h.run(20);
const plain = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && !c.dmg.isSkill);
assert.ok(plain.length > 5);
for (const c of plain) assert.ok([1.05, 1.05 * pierce(6, true, Y).atk_scale].some((k) => Math.abs(c.amount - u.base.atk * k) < 1e-6), `UP: two tiles above her is her line (${c.amount})`);
done(h);
});
+2 -2
View File
@@ -19,7 +19,7 @@ through.
| `fields` | 22 battles: every Final Assault / Hidden Core leader on a pair and a solo template (shared pool, 200 s cap) and the 联防 field with 1 and 2 helpers on its own map (carried HP / SP, a knocked-out operator, two leakers' enemies) |
| `matches` | 17 matches to the end in virtual time: 16 bot-only (solo 标准 / 险境 / 绝境 / 终极 × 2 seeds, co-op 2 / 3 / 4, one server-run combat match, two runs boosted to the Hidden Core) and one co-op match whose human seat (AI 托管, offline) does not own 7 NORMAL chess — they fight as their 补位 stand-ins (its digest lists them per round, `standIns`) |
| `standins` | 10 battles: every NORMAL chess record (normal and elite, 110) fielded as its 补位 stand-in (`standIn: true`: the stand-in's body, backup skill / module and kit — all 17 stand-ins, every skill a chess names for them), 12 per battle by strength band, laid out by the stand-in's position on a real stage, against the round's real wave three times over plus 8 ground enemy kinds (melee stand-ins always meet an enemy), an item each, bonds from the board |
| `diy` | 6 battles of 自选 pieces (a DIY slot with its `diy` pick, `shared/diy.js`): every owned 6★ with an operator kit (`kits/index.js OPERATOR_KITS` — 推进之王 so far) in each form of tiers 5 and 6 (normal; elite with no module and with each module) under each skill, then every prototype pick with a kit at its locked selection, normal and elite, 12 per battle against the round's real wave; a new operator kit adds its scenarios |
| `diy` | 12 battles of 自选 pieces (a DIY slot with its `diy` pick, `shared/diy.js`): every owned 6★ with an operator kit (`kits/index.js OPERATOR_KITS` — 推进之王, 黑, 焰狐龙梓兰 and 鸿雪 so far) in each form of tiers 5 and 6 (normal; elite with no module and with each module) under each skill, then every prototype pick with a kit at its locked selection, normal and elite, 12 per battle against the round's real wave; a new operator kit adds its scenarios |
Battles go through the production BattleSpec path (`server/sim/spec.js`, as browsers and the server's headless fields
run them); matches construct `Match` directly with a `VirtualScheduler`. Every option is explicit, so a change to a
@@ -32,7 +32,7 @@ to 2 decimals.
`npm test` runs the fast subset (the scenarios marked `"fast": true`: every chess record with its default loadout,
every stage and every non-leader enemy kind, every bond at its top tier, six fields, six matches, the five stand-in
battles of the normal records, the first 自选 battle of the operator kits and of the prototypes — 61 of the 150). `GOLDEN_FULL=1` checks everything.
battles of the normal records, the first 自选 battle of the operator kits and of the prototypes — 61 of the 156). `GOLDEN_FULL=1` checks everything.
## Workflow
+340 -100
View File
@@ -90,130 +90,130 @@
"snaps": "d7b739d1 f5e170c4 bc463f9c 6ebd80ed 93f8e1ae ea1947a7 9541a28f e8dbac5b b0b3c196 14ae4a64 46640828 01c55ea9 0f261678 f447848b 0aad3542 db5850a3 631cc5ca 455c1dbc 68ad7134 24c71ebb 34a3ee93 37b05130 7a58a87e"
},
"diy-003": {
"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=shwaz/S1 5_diy2_a=shwaz/S2 5_diy1_a=shwaz/S3 5_diy2_b=shwaz/S1 5_diy1_b=shwaz/S2 5_diy2_b=shwaz/S3 5_diy1_b=shwaz/S1/002_shwaz 5_diy2_b=shwaz/S2/002_shwaz 5_diy1_b=shwaz/S3/002_shwaz 5_diy2_b=shwaz/S1/003_shwaz 5_diy1_b=shwaz/S2/003_shwaz 5_diy2_b=shwaz/S3/003_shwaz",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_single_normal",
"round": 12,
"seed": 3865246633,
"waveId": "act1autochess_h05",
"time": 196.5,
"ticks": 5895,
"time": 187.43,
"ticks": 5623,
"reason": "cleared",
"killed": 8,
"killed": 21,
"total": 93,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"rngDraws": 666,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":8,"total":93,"perfect":false,"leaks":85,"leaksUncounted":0,"coins":0,"dmg":306215,"bossDmg":0,"heal":22436,"deaths":11,"layerGains":{}}},
"players": {"p1":{"killed":21,"total":93,"perfect":false,"leaks":72,"leaksUncounted":0,"coins":0,"dmg":671339,"bossDmg":0,"heal":0,"deaths":12,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,114,0,690,6384,0,13,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,98,1,7246,10191,0,56,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,116,1,16020,8139,0,28,1],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,74,3,34176,12589,0,37,2],
["p1",5,"op","chess_char_5_diy1_a",2,null,871,1,8,1,22261,400,0,129,3],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,12,0,32187,200,0,108,3],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,14,0,30588,200,0,77,2],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1160,1,4,1,71148,200,0,154,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,6,0,0,600,18186,48,3],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,115,0,0,8077,4251,12,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,13,1,40672,2000,0,123,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,15,0,51226,800,0,132,3],
["p1",1,"op","chess_char_5_diy1_a",0,null,593,1,6,1,45541,800,0,66,13],
["p1",2,"op","chess_char_5_diy2_a",1,null,268,1,11,1,53456,1125,0,69,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,468,1,14,0,36190,925,0,63,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",760,1,4,4,102430,925,0,106,21],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,5,4,95519,1685,0,69,2],
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,80,0,30978,3370,0,37,2],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_shwaz",123.06,1,12,4,89648,1692,0,88,17],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_shwaz",615,1,15,3,152691,1200,0,110,3],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_shwaz",1815,1,117,1,27678,5445,0,19,1],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_shwaz",1685,1,110,1,8987,3370,0,10,1],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_shwaz",1685,1,112,1,10406,3370,0,12,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_shwaz",0,0,77,1,17815,3370,0,21,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1267_nhpbr_2":[36,8,28,17650,131613],"enemy_1270_nhstlk_2":[27,0,27,15235,63417],"enemy_9006_actoxi":[30,0,30,16896,111185]},
"hooks": [1,116,5895,1089,1089,1215,1215,60,19,104,26,24,0,28385,254,28,93,85,1,19,0,0,0],
"events": {"atk":1089,"deploy":20,"die":19,"dmg":1215,"fx":8,"heal":60,"leak":85,"skill":50,"spawn":108,"status":504},
"fx": {"zone":8},
"snaps": "b1b2dda9 6ac577d6 84811e80 eb6305ea 52a34c95 c2fd4731 45525f0b dbcc3ddb ddfcbc11 0874e859 206cd671 4c83f44b f4f5331a 194da7d5 1fe13dbf 6b7331b4 4cf19629 7bae5e13 5b7078fc 65a8a1f5"
"enemies": {"enemy_1267_nhpbr_2":[36,15,21,10329,223304],"enemy_1270_nhstlk_2":[27,2,25,10394,179967],"enemy_9006_actoxi":[30,4,26,4960,268068]},
"hooks": [1,117,5623,708,708,997,997,0,33,105,63,62,0,12475,187,17,93,72,1,33,0,0,0],
"events": {"atk":708,"deploy":21,"die":33,"dmg":997,"fx":221,"leak":72,"skill":125,"spawn":108,"status":157},
"fx": {"crit":202,"zone":19},
"snaps": "ab432541 10d870b4 03dda609 8c408032 53172b41 7450209f 557878c6 cd8a7a01 5fcc3a7e ffa61f1e 1d10cc54 c7efcb48 ddf8d6ed a1c11a82 ec6e12fc afd5e081 56ef6663 fb12f8c7 cce38529"
},
"diy-004": {
"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=shwaz/S1 6_diy2_a=shwaz/S2 6_diy1_a=shwaz/S3 6_diy2_b=shwaz/S1 6_diy1_b=shwaz/S2 6_diy2_b=shwaz/S3 6_diy1_b=shwaz/S1/002_shwaz 6_diy2_b=shwaz/S2/002_shwaz 6_diy1_b=shwaz/S3/002_shwaz 6_diy2_b=shwaz/S1/003_shwaz 6_diy1_b=shwaz/S2/003_shwaz 6_diy2_b=shwaz/S3/003_shwaz",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m04",
"modeId": "mode_multi_abyss",
"round": 12,
"round": 13,
"seed": 1210847140,
"waveId": "act1autochess_h05",
"time": 224.77,
"ticks": 6743,
"waveId": "act1autochess_h06",
"time": 211.87,
"ticks": 6356,
"reason": "cleared",
"killed": 21,
"total": 117,
"killed": 0,
"total": 111,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 350,
"rngDraws": 541,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":21,"total":117,"perfect":false,"leaks":96,"leaksUncounted":0,"coins":0,"dmg":374057,"bossDmg":0,"heal":25487,"deaths":23,"layerGains":{}}},
"players": {"p1":{"killed":0,"total":111,"perfect":false,"leaks":111,"leaksUncounted":0,"coins":0,"dmg":541105,"bossDmg":0,"heal":0,"deaths":17,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,116,0,0,5208,0,0,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",100.37,1,143,0,0,6642,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,1060.28,1,145,0,4912,3527,0,0,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,98,5,85077,5533,0,0,3],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,104,0,0,8356,0,0,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,100,0,0,20296,0,0,0],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,132,0,16051,2544,0,16,1],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,135,5,91286,3174,0,37,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,77,0,6653,2744,0,3,0],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,7,11,170077,1749,0,21,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,144,0,0,5333,10172,24,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,94,0,0,8375,15315,21,1],
["p1",1,"op","chess_char_6_diy1_a",0,null,1393,1,4,0,90678,0,0,119,23],
["p1",2,"op","chess_char_6_diy2_a",1,null,1393,1,5,0,116021,0,0,124,3],
["p1",3,"op","chess_char_6_diy1_a",2,null,1393,1,15,0,123247,0,0,114,4],
["p1",4,"op","chess_char_6_diy2_b",0,"none",1685,1,137,0,24574,3370,0,26,5],
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,76,0,35576,3370,0,33,1],
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,125,0,26512,5055,0,29,0],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_shwaz",1855,1,135,0,12706,5565,0,12,1],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_shwaz",1855,1,118,0,51370,3710,0,37,1],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_shwaz",0,0,67,0,21957,3710,0,13,1],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_shwaz",0,0,9,0,13564,1685,0,12,2],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_shwaz",0,0,11,0,10868,1685,0,10,0],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_shwaz",0,0,13,0,14031,1685,0,12,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1005_yokai":[51,16,35,0,83756],"enemy_1005_yokai_2":[54,5,49,24107,111017],"enemy_1005_yokai_3":[12,0,12,42273,179284]},
"hooks": [1,145,6743,501,501,975,936,86,44,140,9,9,0,14181,0,0,117,96,1,44,0,0,39],
"events": {"atk":594,"deploy":25,"die":44,"dmg":936,"fx":87,"heal":78,"leak":96,"skill":18,"spawn":132,"status":8},
"fx": {"aoe":1,"dodge":39,"dp":9,"slash":32,"spGain":6},
"snaps": "6b632c98 e087bd9f f5b9da13 f7038fad bb4d7811 5178d072 0c0de684 872f1892 81773c41 3d296a6a 66f1329a 3a99c8b1 aa011a13 319c8ec5 26231cc7 f808b9be 73dd7977 75857859 817f18b6 d1cdd533 cd927f27 64106a51 89f62784"
"enemies": {"enemy_1010_demon":[54,0,54,16580,311889],"enemy_1320_wdrrl_2":[18,0,18,0,61034],"enemy_1422_lrsldr_2":[39,0,39,9190,257992]},
"hooks": [1,137,6356,579,579,1921,1921,0,17,128,42,42,0,13087,167,16,111,111,1,17,0,0,0],
"events": {"atk":579,"deploy":23,"die":17,"dmg":1921,"fx":167,"leak":111,"skill":84,"spawn":126,"status":188},
"fx": {"crit":167},
"snaps": "070638af 89d4ae9f 63469448 0a388046 3bfe8a59 bbc25451 89815067 d03ae37b 6dea06e2 a9c7fcf8 f97d541b e392c2aa 321550ed 5d91d302 15d98ce6 becf0c8e 696b68cd f250af65 fbd80cc2 23fb161e f3f25187 47efcb02"
},
"diy-005": {
"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=orchd2/S1 5_diy2_a=orchd2/S2 5_diy1_a=orchd2/S3 5_diy2_b=orchd2/S1 5_diy1_b=orchd2/S2 5_diy2_b=orchd2/S3 5_diy1_b=orchd2/S1/002_orchd2 5_diy2_b=orchd2/S2/002_orchd2 5_diy1_b=orchd2/S3/002_orchd2 6_diy1_a=orchd2/S1 6_diy2_a=orchd2/S2 6_diy1_a=orchd2/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m05",
"modeId": "mode_multi_funny",
"round": 13,
"round": 12,
"seed": 823748262,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 63,
"total": 123,
"waveId": "act1autochess_h05",
"time": 205.1,
"ticks": 6153,
"reason": "cleared",
"killed": 39,
"total": 60,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 117,
"rngDraws": 25,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":63,"total":123,"perfect":false,"leaks":60,"leaksUncounted":0,"coins":0,"dmg":274732,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"players": {"p1":{"killed":39,"total":60,"perfect":false,"leaks":21,"leaksUncounted":0,"coins":0,"dmg":612070,"bossDmg":0,"heal":0,"deaths":11,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,1834,1,132,5,19446,3374,0,80,2],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",2142.36,1,141,5,29488,4720,0,101,2],
["p1",3,"op","chess_char_5_diy1_a",0,null,1865,1,154,3,27639,3730,0,42,1],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,152,11,43106,4748,0,39,1],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,135,3,18986,4587,0,18,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1808.52,1,143,18,92036,3903,0,40,4],
["p1",7,"op","chess_char_5_diy1_a",2,null,117.2,1,130,12,39744,6807,0,115,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,1,0,0,0,0,0,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,2638,1,3,0,0,0,0,0,0],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",3531,1,5,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,153,0,1743,2320,0,7,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1120.4,1,145,0,2545,3170,0,7,1]
["p1",1,"op","chess_char_5_diy1_a",0,null,1358,1,10,0,6533,0,0,68,16],
["p1",2,"op","chess_char_5_diy2_a",1,null,1358,1,11,0,14057,0,0,68,7],
["p1",3,"op","chess_char_5_diy1_a",2,null,1358,1,1,1,9167,0,0,77,6],
["p1",4,"op","chess_char_5_diy2_b",0,"none",1643,1,3,0,16918,0,0,104,36],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1643,1,5,5,138677,0,0,127,9],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1643,1,7,1,37314,0,0,95,8],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_orchd2",1643,1,9,0,0,0,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_orchd2",1643,1,12,0,49869,0,0,120,8],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_orchd2",235,1,80,28,278358,4694,0,33,7],
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,78,1,2403,4074,0,31,8],
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,75,1,40303,4074,0,31,5],
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,70,2,18472,4074,0,22,1]
],
"enemies": {"enemy_1019_jshoot_2":[57,37,20,9953,112024],"enemy_1299_ymkilr_2":[27,6,20,25821,123649],"enemy_1422_lrsldr_2":[39,20,19,1584,123193]},
"hooks": [1,154,6900,835,835,3335,3375,0,83,142,12,15,0,16427,38,83,123,59,1,83,0,0,0],
"events": {"atk":851,"deploy":31,"die":83,"dmg":3367,"fx":150,"leak":59,"skill":22,"spawn":135,"status":47},
"fx": {"aoe":83,"displace":3,"dp":26,"slash":38},
"snaps": "aa4b712f 51a2360b 59868653 c815c7eb 7dd47d06 3f3f03f4 89e883c3 60e1e7f6 2355716a 5d734980 75cde05f c0dd657c 4e5bd257 0f8a040d b1aafe05 c04cbb36 e6d37890 8457998a a19143e5 d709bc2b 4283543b 0d9edf54 6ef9963d"
"enemies": {"enemy_1045_hammer_2":[12,4,8,14399,163896],"enemy_1061_zomshd":[48,35,13,2517,487535]},
"hooks": [1,80,6153,804,804,4200,4200,19492,50,71,111,110,0,43797,6,18,60,21,1,50,0,0,0],
"events": {"atk":935,"deploy":20,"die":50,"dmg":4200,"fx":175,"leak":21,"skill":221,"spawn":72,"status":27},
"fx": {"aoe":29,"buff":8,"displace":28,"dragonArrow":21,"quadShot":60,"takeoff":29},
"snaps": "7541895e 9722de68 04c99c01 b3218acc 9261302b 42229a76 9b9dd065 51d9053e 2c4e8079 37c11eec a7817eaa e2c35c85 cb1f5ae1 01c3306d e235b7c5 defc2da3 5fde9f5d fdc6bb1f 47c6c147 e7d867d1 6946f673"
},
"diy-006": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
"about": "6_diy1_b=orchd2/S1 6_diy2_b=orchd2/S2 6_diy1_b=orchd2/S3 6_diy2_b=orchd2/S1/002_orchd2 6_diy1_b=orchd2/S2/002_orchd2 6_diy2_b=orchd2/S3/002_orchd2 5_diy1_a=bgsnow/S1 5_diy2_a=bgsnow/S2 5_diy1_a=bgsnow/S3 5_diy2_b=bgsnow/S1 5_diy1_b=bgsnow/S2 5_diy2_b=bgsnow/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m06",
@@ -221,35 +221,275 @@
"round": 13,
"seed": 2464539579,
"waveId": "act1autochess_h06",
"time": 210.23,
"ticks": 6307,
"time": 209.03,
"ticks": 6271,
"reason": "cleared",
"killed": 5,
"killed": 16,
"total": 81,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 123,
"rngDraws": 25,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":5,"total":81,"perfect":false,"leaks":76,"leaksUncounted":0,"coins":0,"dmg":185202,"bossDmg":0,"heal":4048,"deaths":30,"layerGains":{}}},
"players": {"p1":{"killed":16,"total":81,"perfect":false,"leaks":65,"leaksUncounted":0,"coins":0,"dmg":404038,"bossDmg":0,"heal":0,"deaths":22,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,3,0,11325,1865,0,7,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,6,3,52957,2494,0,16,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,112,0,15444,8178,0,22,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,109,2,51553,12663,0,57,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,113,0,4256,2544,0,15,0],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,105,0,10180,3286,0,19,0],
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,5,0,3060,1372,0,2,0],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,2,0,30589,1819,0,4,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,8,0,0,2028,2464,7,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,61,0,0,3890,1584,6,1],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,58,0,2490,3291,0,7,1],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,60,0,3347,3320,0,8,0]
["p1",1,"op","chess_char_6_diy1_b",0,"none",0,0,95,0,38029,4929,0,16,7],
["p1",2,"op","chess_char_6_diy2_b",1,"none",0,0,81,6,143617,3286,0,24,4],
["p1",3,"op","chess_char_6_diy1_b",2,"none",0,0,90,0,4400,4929,0,7,1],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_orchd2",0,0,103,2,55563,4929,0,17,7],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_orchd2",0,0,32,7,113856,3286,0,14,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_orchd2",0,0,50,0,14303,4929,0,8,1],
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,12,0,948,1369,0,2,0],
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,7,0,6358,1369,0,8,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,9,0,2370,1369,0,5,0],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,11,0,1824,1656,0,3,0],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,4,1,12075,1656,0,11,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,6,0,10696,1656,0,12,1]
],
"enemies": {"enemy_1267_nhpbr_2":[27,5,22,7765,89733],"enemy_1270_nhstlk_2":[36,0,36,14278,68347],"enemy_1272_nhtank_2":[18,0,18,24706,27121]},
"hooks": [1,113,6307,262,262,449,476,16,35,111,6,21,0,5828,86,24,81,76,1,35,0,0,0],
"events": {"atk":283,"deploy":32,"die":35,"dmg":441,"fx":58,"heal":15,"leak":76,"skill":12,"spawn":93,"status":142},
"fx": {"aoe":7,"displace":21,"spGain":6,"zone":24},
"snaps": "1dcf4706 8bd55683 d93e4d02 ab832ad5 132699ab f37b039d f12175ca fe30225b 6b2b77c0 2caefa3a 1018f9e1 a60a2364 f81df3c0 d0f2780e 0e940f53 372c90ef 46af3a81 a6b2adf1 63234976 3b8b7c59 b62aa8d4"
"enemies": {"enemy_1267_nhpbr_2":[27,11,16,100,107273],"enemy_1270_nhstlk_2":[36,4,32,13399,208206],"enemy_1272_nhtank_2":[18,1,17,21864,88559]},
"hooks": [1,103,6271,176,176,643,643,0,38,103,28,28,0,6552,8,18,81,65,1,38,0,0,0],
"events": {"atk":219,"deploy":22,"die":38,"dmg":643,"fx":69,"leak":65,"skill":56,"spawn":93,"status":55},
"fx": {"aoe":7,"buff":10,"dragonArrow":2,"quadShot":14,"takeoff":7,"zone":29},
"snaps": "0ff3da34 eebf2f83 f86ea75a 53271705 e8e17748 10f438be 26f108c4 be78a601 aa0141c1 710b8f1d b8d23f9c 0639175c 4c494674 a177e1bf 9d40de6d d37c62f7 3c00194d b3c78666 d4911850 6fcfee8d 89d8cc2c"
},
"diy-007": {
"about": "5_diy1_b=bgsnow/S1/002_bgsnow 5_diy2_b=bgsnow/S2/002_bgsnow 5_diy1_b=bgsnow/S3/002_bgsnow 5_diy2_b=bgsnow/S1/003_bgsnow 5_diy1_b=bgsnow/S2/003_bgsnow 5_diy2_b=bgsnow/S3/003_bgsnow 6_diy1_a=bgsnow/S1 6_diy2_a=bgsnow/S2 6_diy1_a=bgsnow/S3 6_diy2_b=bgsnow/S1 6_diy1_b=bgsnow/S2 6_diy2_b=bgsnow/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_normal",
"round": 13,
"seed": 1837403644,
"waveId": "act1autochess_h06",
"time": 211.87,
"ticks": 6356,
"reason": "cleared",
"killed": 11,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 169,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":11,"total":123,"perfect":false,"leaks":112,"leaksUncounted":0,"coins":0,"dmg":333377,"bossDmg":0,"heal":0,"deaths":4,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_bgsnow",1786,1,5,0,8790,0,0,50,1],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_bgsnow",1786,1,7,0,14937,0,0,57,12],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_bgsnow",1786,1,9,0,7445,0,0,56,2],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_bgsnow",1656,1,11,0,11838,0,0,51,1],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_bgsnow",1656,1,13,0,18046,0,0,51,11],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_bgsnow",1656,1,6,7,74942,0,0,150,3],
["p1",7,"op","chess_char_6_diy1_a",0,null,1369,1,8,0,3169,0,0,50,1],
["p1",8,"op","chess_char_6_diy2_a",1,null,1369,1,10,2,60625,0,0,126,18],
["p1",9,"op","chess_char_6_diy1_a",2,null,1369,1,12,0,49188,0,0,149,3],
["p1",10,"op","chess_char_6_diy2_b",0,"none",1656,1,14,0,73742,0,0,122,1],
["p1",11,"op","chess_char_6_diy1_b",1,"none",1656,1,133,2,10096,3312,0,15,3],
["p1",12,"op","chess_char_6_diy2_b",2,"none",1656,1,129,0,560,3312,0,9,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_1006_shield_3":[21,0,21,3312,33986],"enemy_1043_zomsbr":[66,11,55,1374,257145],"enemy_1402_tgshd_2":[36,0,36,1938,42246]},
"hooks": [1,141,6356,899,899,985,985,20538,15,127,57,53,0,27860,0,6,123,112,1,15,0,0,0],
"events": {"atk":899,"deploy":16,"die":15,"dmg":985,"fx":524,"leak":112,"skill":110,"spawn":137,"status":130},
"fx": {"crit":45,"expose":479},
"snaps": "7c8b1a6a 446c0c56 799157ea ab79eed5 b2b28700 088603cd 775e8cc3 7164f5f2 b9ea3330 84283433 d97cfa14 7f7b5348 7e410aca c92d3189 562e59c3 5fe6997e fa502c61 68f955eb b93119af e528980f b7781968 91c82542"
},
"diy-008": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=bgsnow/S1/002_bgsnow 6_diy2_b=bgsnow/S2/002_bgsnow 6_diy1_b=bgsnow/S3/002_bgsnow 6_diy2_b=bgsnow/S1/003_bgsnow 6_diy1_b=bgsnow/S2/003_bgsnow 6_diy2_b=bgsnow/S3/003_bgsnow 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_multi_hard",
"round": 13,
"seed": 3478524561,
"waveId": "act1autochess_h06",
"time": 221.23,
"ticks": 6637,
"reason": "cleared",
"killed": 4,
"total": 147,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 95,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":4,"total":147,"perfect":false,"leaks":143,"leaksUncounted":0,"coins":0,"dmg":347594,"bossDmg":0,"heal":0,"deaths":22,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,98,0,165,4256,0,6,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,143,0,5244,5816,0,38,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,172,0,3111,7692,0,15,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",1870.15,1,173,0,15330,8132,0,18,1],
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_bgsnow",1826,1,2,2,94832,0,0,120,1],
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_bgsnow",1826,1,3,1,114659,0,0,138,22],
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_bgsnow",1826,1,4,1,81377,0,0,144,3],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_bgsnow",0,0,156,0,7893,4968,0,18,0],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_bgsnow",0,0,83,0,20268,3312,0,23,5],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_bgsnow",0,0,56,0,510,3312,0,11,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,168,0,286,3813,0,11,1],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,163,0,3919,4530,0,29,1]
],
"enemies": {"enemy_1166_dusbr_2":[39,3,36,11279,152576],"enemy_1169_duphlx_2":[57,1,56,13781,153310],"enemy_1170_dushld_2":[51,0,51,20770,41707]},
"hooks": [1,173,6637,691,691,741,741,0,26,169,35,34,0,18241,0,32,147,143,1,26,0,0,0],
"events": {"atk":691,"deploy":26,"die":26,"dmg":741,"fx":29,"leak":143,"skill":69,"spawn":159,"status":147},
"fx": {"crit":29},
"snaps": "f7fcf200 6e3880e4 657eff54 19808845 e8ab803f a640a48f d7a1b7d6 8798410b f974d6cf d50c53a8 9a9e9c5e a0517d03 32dca213 e2c669e7 317335a1 17d7a4a0 ce0f4bd1 45f8dcf4 dd177c3c c281f902 4c1a8a24 a398d8dc 9696d7af"
},
"diy-009": {
"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_m02",
"modeId": "mode_single_normal",
"round": 12,
"seed": 2612428600,
"waveId": "act1autochess_h05",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 18,
"total": 42,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 100,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":18,"total":42,"perfect":false,"leaks":24,"leaksUncounted":0,"coins":0,"dmg":60423,"bossDmg":0,"heal":11501,"deaths":27,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,3,0,0,2604,0,0,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,5,0,0,3404,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,64,0,12281,4302,0,0,2],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,69,1,19550,7605,0,0,3],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,47,0,0,7726,0,0,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,44,0,0,6822,0,0,0],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,52,0,0,2266,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,50,0,16580,4004,0,35,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,67,0,0,5674,2020,7,1],
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,71,0,0,5543,9480,20,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,70,0,5609,3380,0,15,1],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,68,0,6403,3963,0,17,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1005_yokai_2":[18,18,0,830,5156],"enemy_1005_yokai_3":[12,0,11,28478,30434],"enemy_9009_acfort":[12,0,12,27987,24833]},
"hooks": [1,71,6900,462,462,467,442,27,45,68,10,10,0,12457,32,0,42,23,1,28,0,0,25],
"events": {"atk":462,"deploy":27,"die":45,"dmg":442,"fx":121,"heal":27,"leak":23,"skill":20,"spawn":56,"status":107},
"fx": {"beam":55,"dodge":25,"dp":6,"slash":35},
"snaps": "8f962d08 3056b267 beb14b31 2394cb94 2b9de697 194c89b5 3a08d0d2 1d09774e 959dc47d 270d0004 f0773a73 3566bde9 b4abb17e 8201dc8f a0766de3 267f5f64 24e29cf1 b763dc25 c2f98e66 db226502 88b350d1 0dc0611a 604e4306"
},
"diy-010": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m03",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 4252812069,
"waveId": "act1autochess_h05",
"time": 208.93,
"ticks": 6268,
"reason": "cleared",
"killed": 0,
"total": 114,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 113,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":114,"perfect":false,"leaks":114,"leaksUncounted":0,"coins":0,"dmg":100335,"bossDmg":0,"heal":6344,"deaths":30,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,5,0,15630,1865,0,9,1],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,7,0,18138,3029,0,5,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,145,0,25048,10069,0,63,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",0,0,147,0,19052,13642,0,19,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,445.63,1,148,0,6079,3370,0,15,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,141,0,5804,5761,0,12,0],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,13,0,1663,1372,0,2,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,73,0,1993,5153,0,2,0],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,4,0,0,1657,3351,5,1],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,10,0,0,1414,2993,6,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,6,0,2988,1160,0,7,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,8,0,3939,1430,0,7,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1019_jshoot_2":[51,0,51,9812,8520],"enemy_1381_winman":[27,0,27,9427,90722],"enemy_1389_winbab_2":[36,0,36,30683,1093]},
"hooks": [1,148,6268,331,331,310,319,18,30,144,5,21,0,5538,61,21,114,114,1,30,0,0,0],
"events": {"atk":343,"deploy":31,"die":30,"dmg":304,"fx":16,"heal":16,"leak":114,"skill":10,"spawn":129,"status":58},
"fx": {"displace":8,"spGain":8},
"snaps": "2971e9ff e77895c1 13abdad5 03f3c6e3 025969bd f22bbc34 92c20fec fd2f44ee 674a7f68 561c16b2 a94168d2 1b34b5cf 1d6ac33f b46b69d8 57111593 2c0e5d03 c4e78702 e9d8f752 1a9f888a c9d4b090 82ba1d23"
},
"diy-011": {
"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": "act2autochess_m04",
"modeId": "mode_multi_funny",
"round": 13,
"seed": 538212341,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 83,
"total": 144,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 754,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":83,"total":144,"perfect":false,"leaks":61,"leaksUncounted":0,"coins":0,"dmg":727357,"bossDmg":0,"heal":57340,"deaths":20,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,7,8,27898,13166,0,80,2],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",2829.8,1,167,7,27469,8851,0,66,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,6,16,140111,3022,0,54,6],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,144,14,159200,6358,0,8,5],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,143,2,11845,7595,0,14,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,136,20,76014,33300,0,54,3],
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,3,2,30581,1272,0,62,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,4,12,238233,1643,0,161,2],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,153,1,9117,5500,0,8,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,169,0,6889,7367,0,6,0],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,5,0,0,1644,40995,55,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,168,0,0,8573,16345,21,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_2021_syfish":[99,73,24,10687,302163],"enemy_2025_syufo":[45,10,35,87604,438052]},
"hooks": [1,169,6900,1412,1412,2733,2987,143,103,162,26,26,0,27540,0,64,144,59,1,104,0,33,62],
"events": {"atk":1560,"deploy":23,"die":103,"dmg":2987,"fx":320,"heal":134,"leak":59,"skill":52,"spawn":158,"status":123},
"fx": {"aoe":80,"burst":33,"dodge":62,"dp":41,"slash":88,"spGain":16},
"snaps": "55a12ce8 172ad35e b858882a 66672e2c bf89e3e3 9be6cb9e a0514ec0 d7b44a56 39783f17 ba8e20c8 09cd87af 746573df b9e27578 e7dd7dfc cb04e699 92d3e0b3 7bdb1773 b6882cdd a0d06e3c 5eab91e4 24e99439 b044b69e 7d8c5fa0"
},
"diy-012": {
"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_m01",
"modeId": "mode_single_hard",
"round": 13,
"seed": 3192456040,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 10,
"total": 117,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 178,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":10,"total":117,"perfect":false,"leaks":107,"leaksUncounted":0,"coins":0,"dmg":228417,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,145,1,34634,5595,0,56,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,143,7,57826,7482,0,40,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,146,0,22026,9232,0,68,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,147,2,58947,13356,0,67,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,4,0,23667,0,0,150,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,5,0,31317,0,0,155,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]
],
"enemies": {"enemy_1166_dusbr_2":[39,8,31,3534,97703],"enemy_1170_dushld_2":[39,2,36,10197,64140],"enemy_9011_acrefr":[39,0,37,21934,66574]},
"hooks": [1,147,6900,622,622,893,944,0,33,137,8,23,0,11211,11900,38,117,104,1,33,0,0,0],
"events": {"atk":668,"deploy":27,"die":33,"dmg":838,"fx":20,"leak":104,"skill":14,"spawn":126,"status":1135},
"fx": {"displace":16,"dodge":4},
"snaps": "d28bb789 63eb985d 4d0f9eff 5acb7032 16fffd96 ad116d10 106aebb2 764edc40 ffc24dbd d20cfab0 2ec7d68f c9765510 e0639444 cbfcf6d4 de2fc904 6c97bbcb 69043b03 56f9d1aa c7b63229 79cdb25e 4e2e6e65 d377e48b d6594021"
}
}
}