feedback5: merge O23 — 自选 kits for 白铁 (devices, 铁钳号), 娜斯提 (three 支援装置, the built 高台) and 死芒 (复燃 servants, special form); engine (additive): an ally our attacks select (queries / ai, after enemies in target order, never healed); golden: only diy moved

This commit is contained in:
sganggs committed 2026-10-06 14:17:00 +08:00
commit 9f68afe9f9
12 files changed
+2909 -133

No files matched your search

+457
View File
@@ -0,0 +1,457 @@
// test/content/op_ironmn.test.js — the 自选 operator kit of 白铁 (char_4072_ironmn, 6★ 工匠; kit
// server/sim/content/kits/ops/op-ironmn.js) and of his <支援装置> 白铁™多功能平台 (token_10027_ironmn_pile1 / pile2) and 铁钳号·原型机
// (token_10027_ironmn_pile3), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy; the devices as the placed
// hand pieces of his player) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7)
// with no module, CRA-X 铁钳号·爬行者 or CRA-Y 海布里印记 at stage 1 (tier 5) / 3 (tier 6). Numbers from data/backups.json; the
// fidelity checklist of kits/README.md (the engine's ally targets themselves: test/sim/feedback5-o23-engine.test.js).
// Run: node --test test/content/op_ironmn.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, diyRecordOf } 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 IRON = 'char_4072_ironmn';
const P1 = 'token_10027_ironmn_pile1', P2 = 'token_10027_ironmn_pile2', CRAB = 'token_10027_ironmn_pile3';
const TOK = [P1, P2, CRAB];
const FORMS = BACKUPS.units[IRON].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_ironmn', Y = 'uniequip_003_ironmn';
const S1 = 'skchr_ironmn_1', S2 = 'skchr_ironmn_2', S3 = 'skchr_ironmn_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);
/** A device's variant for a form, with the pick's skill / module (getDiyToken). */
const tokOf = (id, tier, elite, skill, mod) => {
let v = BACKUPS.tokens[id].variants[`${IRON}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
return v;
};
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = {
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }),
enemy_hit: dummy('enemy_hit', { atk: 1000, bat: 1, blockCnt: 1 }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, X, Y].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/**
* 白铁 as uid 1 at (row, col) facing `dir`; `pieces` = his device pieces [[r, c, dir?]] (uids 10…, the picked skill's
* device); `others` = more units.
*/
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, dir = 'RIGHT', pieces = [], others = [], seed = 5, dp = 99 } = {}) {
const units = [
{ uid: 1, diy: { slot: SLOT[tier], charId: IRON, skillIndex: skill, uniEquipId: mod }, elite, row, col, dir },
...pieces.map(([r, c, d], i) => ({ uid: 10 + i, kind: 'token', tokenId: TOK[skill], ownerUid: 1, row: r, col: c, dir: d ?? 'RIGHT' })),
...others,
];
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'death', 'deploy', 'statusApplied', 'spGain', 'heal'], captureNoisy: true,
units,
});
h.step();
h.b.players[0].dp = dp;
const u = h.unit(1);
const devs = h.b.allyUnits.filter((a) => a.kind === 'token' && a.defId === TOK[skill]).sort((a, b) => a.uid - b.uid);
return { h, u, devs };
}
const st = (u) => u.mem.craftsman;
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
/** Force every rng.chance roll to `v` (returns the probabilities asked for). */
function forceChance(h, v) {
const asked = [];
h.b.rng.chance = (p) => { asked.push(p); return v; };
return asked;
}
test('白铁 in every 自选 form: his kit (all three skills authored), stats + module attributes, 1-1, blocks 2, melee physical ground-only, 维多利亚, no 特质, the data triggers', () => {
assert.equal(OPERATOR_KITS[IRON], KITS[IRON]);
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], [IRON, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.aspd + (m?.attr.aspd ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [2, 'melee', false, 'phys', 'craftsman', 1.5], `${label(f)}: 工匠`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
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 him`);
done(h);
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'DEFAULT']);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr],
[{ atk: 25, def: 25, aspd: 5 }, { atk: 45, def: 45, aspd: 5 }, { maxHp: 260, def: 45 }, { maxHp: 400, def: 60 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.deepEqual(diyRecordOf(CHESS[SLOT[5]], { charId: IRON, skillIndex: 0 }, data).bonds, ['victoriaShip']);
assert.ok(KITTED_CHARS.includes(IRON));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(IRON), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: IRON, skillIndex: 2, uniEquipId: Y } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: IRON, skillIndex: 2, uniEquipId: Y } } });
});
test('<支援装置> hand pieces: the picked skill\'s device only (variant sources), deploy limit 1 in the data (official 1 + max_deploy_count 1 = 2: reported), the module costs / redeploy times', () => {
for (const [skill, id] of TOK.entries()) {
for (const s of [0, 1, 2]) assert.equal(tokOf(id, 6, true, s, null).sources.includes('skill'), s === skill, `${id} under S${s + 1}`);
const v = tokOf(id, 6, true, skill, null);
assert.equal(v.stats.deployLimit, 1, `${id}: the data's deploy limit`);
assert.equal(v.talents.find((t) => t.bb.max_deploy_count != null).bb.max_deploy_count, 1, `${id}: the hidden max_deploy_count (E2 "最多可部署2个")`);
}
// CRA-X 部署费用 −1 / −1 / −4, CRA-Y 再部署时间 −5 / −5 / −10 (PRTS module page)
const cost = (mod) => TOK.map((id, s) => tokOf(id, 6, true, s, mod).stats.cost);
const resp = (mod) => TOK.map((id, s) => tokOf(id, 6, true, s, mod).stats.respawnTime);
assert.deepEqual([cost(null), cost(X), cost(Y)], [[3, 3, 10], [2, 2, 6], [3, 3, 10]]);
assert.deepEqual([resp(null), resp(X), resp(Y)], [[10, 10, 20], [10, 10, 20], [5, 5, 10]]);
});
test('战地工程师 / the device rules: stock 3 (CRA-X 4) after his deployment; a device that left comes back on its tile after its redeploy time, paying its cost and one stock; none without stock or DP; his devices leave with him and return after him', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, X], [6, true, Y]]) {
const f = label([tier, elite, mod]);
const { h, u, devs } = field({ tier, elite, mod, skill: 0, pieces: [[11, 7]], dp: 99 });
const d = devs[0];
const v = tokOf(P1, tier, elite, 0, mod);
const cap = 3 + (mod === X ? 1 : 0);
assert.deepEqual([st(u).stock, st(u).cap, d.alive, d.tileR, d.tileC], [cap, cap, true, 11, 7], `${f}: deployed for free with the board, stock ${cap}`);
assert.deepEqual([d.base.cost, d.base.respawnTime], [v.stats.cost, v.stats.respawnTime], `${f}: the variant's cost / redeploy time`);
assert.ok(d.s.flags.untargetable && d.s.flags.invulnerable && d.s.flags.noHeal, `${f}: 不会受到攻击, 无敌, 禁疗`);
forceChance(h, false);
h.b.kill(d, null);
assert.equal(d.alive, false);
h.run(v.stats.respawnTime - 0.3);
assert.equal(d.alive, false, `${f}: not before its redeploy time`);
h.run(0.6);
assert.deepEqual([d.alive, d.tileR, d.tileC, st(u).stock, h.b.players[0].dp], [true, 11, 7, cap - 1, 99 - v.stats.cost], `${f}: back on its tile, −${v.stats.cost} DP, −1 stock`);
// no stock ⇒ it waits
st(u).stock = 0;
h.b.kill(d, null);
h.run(v.stats.respawnTime + 2);
assert.equal(d.alive, false, `${f}: no stock, no return`);
st(u).stock = 1;
h.run(0.5);
assert.ok(d.alive, `${f}: back as soon as he holds one`);
// no DP ⇒ it waits
st(u).stock = 1;
h.b.players[0].dp = 0;
h.b.kill(d, null);
h.run(v.stats.respawnTime + 2);
assert.equal(d.alive, false, `${f}: no DP, no return`);
h.b.players[0].dp = 50;
h.run(0.5);
assert.ok(d.alive, `${f}: back once the DP is there`);
// he leaves ⇒ his devices go; his return refills the stock and brings them back
st(u).stock = 0;
h.b.kill(u, null);
assert.equal(d.alive, false, `${f}: gone with him (die_to_kill_token)`);
h.b.players[0].dp = 99;
assert.ok(h.runUntil(() => u.alive, 200), `${f}: he redeploys`);
assert.equal(st(u).stock, cap, `${f}: stock refilled (charge_token[born])`);
assert.ok(h.runUntil(() => d.alive, 30), `${f}: the device back after him`);
assert.equal(st(u).stock, cap - 1);
done(h);
}
});
test('节约经费: a device of his leaving his x-4 ⇒ prob (0.7; CRA-X stage 3 0.9) ⇒ +1 stock (capped); none when the roll fails, outside his x-4 or when it goes with him', () => {
for (const [tier, elite, mod, prob] of [[5, false, null, 0.7], [5, true, X, 0.7], [6, true, X, 0.9], [6, true, Y, 0.7]]) {
const f = label([tier, elite, mod]);
const { h, u, devs } = field({ tier, elite, mod, skill: 0, pieces: [[11, 6], [10, 8]], dp: 99 });
const [near, far] = devs;
const cap = st(u).cap;
st(u).stock = 1;
let asked = forceChance(h, true);
h.b.kill(near, null);
assert.deepEqual([asked, st(u).stock], [[prob], 2], `${f}: on his x-4 — rolled ${prob}, +1`);
asked = forceChance(h, true);
h.b.kill(far, null);
assert.deepEqual([asked, st(u).stock], [[], 2], `${f}: two tiles away — no roll`);
h.run(25);
assert.ok(near.alive && far.alive);
st(u).stock = 1;
forceChance(h, false);
h.b.kill(near, null);
assert.equal(st(u).stock, 1, `${f}: a failed roll`);
h.run(15);
st(u).stock = cap;
forceChance(h, true);
h.b.kill(near, null);
assert.equal(st(u).stock, cap, `${f}: capped at ${cap}`);
h.run(15);
st(u).stock = 0;
asked = forceChance(h, true);
h.b.kill(u, null);
assert.deepEqual([asked, st(u).stock], [[], 0], `${f}: leaving with him — no roll`);
done(h);
}
});
test('S1 极致火力 + 白铁™多功能平台: ATK +12 % on the operator it faces (×2.5 / ×3 while S1 runs); S1 16 / 17 s, 140 % / 170 % ATK attacks, +1 stock at the start, every device destroyed at its end', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const v = tokOf(P1, tier, elite, 0, null);
const { h, u, devs } = field({ tier, elite, skill: 0, pieces: [[11, 4], [9, 8]], others: [{ uid: 3, chessId: 'chess_char_1_08_a', row: 11, col: 5 }] });
const tx = h.unit(3);
const [d] = devs;
h.run(0.2);
assert.deepEqual([u.skill.rule, sk.duration, sk.bb['attack@atk_scale'], sk.bb.fake_scale, v.skill.bb.talent_scale, v.talents[0].bb.atk], ['DEFAULT', elite ? 17 : 16, elite ? 1.7 : 1.4, elite ? 3 : 2.5, elite ? 3 : 2.5, 0.12], `T${tier}`);
assert.deepEqual(tx.findBuff(`ironmn:pile1:${d.id}`)?.mods, { atkPct: 0.12 }, `T${tier}: 支援火力 on the operator it faces`);
approx(tx.s.atk, tx.base.atk * 1.12, `T${tier}: ATK +12 %`);
assert.equal(u.findBuff(`ironmn:pile1:${d.id}`), null, `T${tier}: not on 白铁 (not on its range)`);
st(u).stock = 1;
forceChance(h, false);
u.skill.gainSp(999);
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in his range`);
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at the start`);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.02);
h.step(2);
approx(tx.s.atk, tx.base.atk * (1 + 0.12 * sk.bb.fake_scale), `T${tier}: ×${sk.bb.fake_scale} while S1 runs`);
const n0 = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e).length;
h.run(3);
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e).slice(n0);
assert.ok(hits.length >= 1);
for (const c of hits) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'], `T${tier}: ${sk.bb['attack@atk_scale'] * 100} % ATK`);
const alive0 = devs.filter((x) => x.alive).length;
assert.equal(alive0, 2);
h.runUntil(() => !u.skill.active, sk.duration + 1);
h.step();
assert.deepEqual(devs.map((x) => x.alive), [false, false], `T${tier}: 技能结束时所有场上的装置被销毁`);
assert.equal(tx.findBuff(`ironmn:pile1:${d.id}`), null, `T${tier}: its buff gone with it`);
assert.ok(h.runUntil(() => devs.every((x) => x.alive), 30), `T${tier}: both back (stock 2)`);
approx(tx.s.atk, tx.base.atk * 1.12, `T${tier}: ×1 after the skill`);
done(h);
}
});
test('S2 高效补给 + 白铁™多功能平台: +1 SP every 3.5 s to the operator it faces (2.5 / 2 s while S2 runs), the device loses 0.4 % (0.8 %) max HP / s; S2 30 s, ATK / DEF +25 / 40 %, every blocked enemy at once, +1 stock at its end', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
const v = tokOf(P2, tier, elite, 1, null);
const { h, u, devs } = field({ tier, elite, skill: 1, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
const yak = h.unit(3);
const [d] = devs;
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.def, v.talents[0].bb['default.interval'], v.skill.bb['s2.interval'], v.talents.find((t) => t.bb.hp_ratio).bb.hp_ratio, v.skill.bb['s2.hp_ratio']],
[30, elite ? 0.4 : 0.25, elite ? 0.4 : 0.25, 3.5, elite ? 2 : 2.5, 0.004, 0.008], `T${tier}`);
const gifts = () => h.hooksOf('spGain').filter((c) => c.unit === yak && c.reason === 'device').map((c) => c.t);
yak.skill.sp = 0;
h.run(11);
const g = gifts();
assert.equal(g.length, 3, `T${tier}: 3 SP gifts in 11 s`);
approx(g[1] - g[0], 3.5, `T${tier}: every 3.5 s`, 0.02);
approx(d.hp, d.s.maxHp * (1 - 0.004 * Math.floor(h.b.time)), `T${tier}: 0.4 % / s`, 0.01);
// S2: two blocked enemies at once, ATK / DEF, interval and bleed
const a = h.spawn('enemy_dummy', { pos: [10, 6] });
const b = h.spawn('enemy_walk', { routeIndex: 0 });
void b;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
approx(u.s.def, u.base.def * (1 + sk.bb.def), `T${tier}: DEF`);
assert.deepEqual(u.skill.spec.attack, { hitAllBlocked: true }, `T${tier}: 攻击所有阻挡的敌人`);
const hp0 = d.hp, t0 = h.b.time;
yak.skill.sp = 0;
const n0 = gifts().length;
h.run(10.05);
const g2 = gifts().slice(n0);
approx(g2[1] - g2[0], v.skill.bb['s2.interval'], `T${tier}: every ${v.skill.bb['s2.interval']} s while S2 runs`, 0.02);
approx(hp0 - d.hp, d.s.maxHp * 0.008 * Math.floor(h.b.time - t0 + 1e-9), `T${tier}: 0.8 % / s while S2 runs`, 0.05);
void a;
st(u).stock = 1;
h.runUntil(() => !u.skill.active, 25);
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at its end`);
done(h);
}
// S2 hits every enemy it blocks
const { h, u } = field({ tier: 6, elite: true, skill: 1, row: 9, col: 5 });
h.spawn('enemy_walk', { routeIndex: 0 });
h.run(0.4);
h.spawn('enemy_walk', { routeIndex: 0 });
assert.ok(h.runUntil(() => u.blocking.length === 2, 40), 'two blocked');
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
const n0 = h.hooksOf('damaged').length;
h.run(4);
const ids = new Set(h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.dmg?.isAttack).map((c) => c.target.id));
assert.equal(ids.size, 2, 'both blocked enemies hit');
done(h);
});
test('S3 铁钳号·原型机: 30 s, ATK +25 / 40 %, ASPD +25 / 40, +1 stock at the start; the 铁钳号 is our attacks\' last target (an enemy first), takes no damage, +1 SP per hit, a 工匠\'s hit heals it 1 %; enemies never target it; immune to the PRTS statuses', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const v = tokOf(CRAB, tier, elite, 2, null);
// the crab faces 白铁's front tile; 德克萨斯 (UP) has it on her range
const { h, u, devs } = field({ tier, elite, skill: 2, pieces: [[10, 6]], others: [{ uid: 3, chessId: 'chess_char_1_08_a', row: 9, col: 6, dir: 'UP' }] });
const tx = h.unit(3);
const [c] = devs;
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.attack_speed, v.skill.spCost, v.skill.spType, v.skill.bb.hp_ratio, v.trait.bb.hp_ratio, v.stats.tauntLevel],
[30, elite ? 0.4 : 0.25, elite ? 40 : 25, 5, 'INCREASE_WHEN_TAKEN_DAMAGE', 0.025, 0.01, -2], `T${tier}`);
assert.ok(h.b.isAllyTarget(c), `T${tier}: an ally target`);
assert.ok(c.s.flags.untargetable && c.s.flags.isolated && c.s.flags.noHeal && c.s.flags.noDisplace && !c.s.flags.invulnerable, `T${tier}: 不会受到攻击 (enemies), 孤立, 禁疗, 失衡免疫, not 无敌`);
h.run(3);
const hitsBy = (src) => h.hooksOf('damaged').filter((x) => x.target === c && x.source === src).length;
const atkOn = (src) => h.events.filter((e) => e[0] === 'atk' && e[1] === src.id && e[2] === c.id).length;
assert.ok(atkOn(u) > 0 && atkOn(tx) > 0, `T${tier}: 白铁 and 德克萨斯 attack it (no enemy around)`);
assert.equal(hitsBy(u) + hitsBy(tx), 0, `T${tier}: no damage reaches it`);
assert.equal(c.hp, c.s.maxHp);
assert.equal(c.skill.sp, 5, `T${tier}: 5 SP (full) after 3 s of hits — 受击回复`);
assert.ok(h.hooksOf('spGain').filter((x) => x.unit === c && x.reason === 'hurt').length >= 5, `T${tier}: one SP per hit`);
// an enemy in an attacker's range comes first (one on the crab's tile, in 白铁's range)
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.step();
const a0 = atkOn(u);
h.run(3);
assert.ok(h.events.filter((x) => x[0] === 'atk' && x[1] === u.id && x[2] === e.id).length >= 1, `T${tier}: 白铁 attacks the enemy`);
assert.equal(atkOn(u), a0, `T${tier}: not the crab meanwhile`);
h.b.kill(e, null);
h.step();
// heal from a 工匠's hit only
c.hp = c.s.maxHp * 0.5;
const healBy = () => h.hooksOf('heal').filter((x) => x.target === c).length;
const h0 = healBy();
h.b.dealDamage(u, c, { amount: 100, type: 'phys', isAttack: true });
approx(c.hp, c.s.maxHp * 0.51, `T${tier}: 白铁 heals it 1 %`);
h.b.dealDamage(tx, c, { amount: 100, type: 'phys', isAttack: true });
approx(c.hp, c.s.maxHp * 0.51, `T${tier}: 德克萨斯 does not`);
assert.equal(healBy() - h0, 1);
// statuses PRTS lists, element, enemies' attacks
for (const s of ['stun', 'cold', 'freeze', 'levitate', 'bind', 'silence', 'sleep']) assert.equal(h.b.applyStatus(c, s, { duration: 3, source: e }), false, `T${tier}: ${s} immune`);
// S3 itself (an enemy in his range: the data's DEFAULT)
h.spawn('enemy_dummy', { pos: [10, 6] });
st(u).stock = 1;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
assert.equal(u.s.aspd, u.base.aspd + sk.bb.attack_speed, `T${tier}: ASPD`);
approx(u.skill.timeLeft, 30, `T${tier}: 30 s`, 0.05);
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at the start`);
done(h);
}
});
test('铁钳号·原型机\'s skill (5 SP, an enemy in its 3-13, flyers too): −2.5 % max HP (流失), then 白铁\'s ATK × 230 % / 260 % physical on the first enemy and × 110 % / 120 % on every other within 1.1 of it; 团结的力量: the operator behind it takes ×0.8 physical (not arts)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const v = tokOf(CRAB, tier, elite, 2, null);
const { h, u, devs } = field({ tier, elite, skill: 2, row: 12, col: 3, pieces: [[10, 5]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 10, col: 4 }] });
const yak = h.unit(3);
const [c] = devs;
assert.deepEqual([v.skill.bb.atk_scale, v.skill.bb.atk_scale_2, v.talents[0].bb.damage_resistance], [elite ? 2.6 : 2.3, elite ? 1.2 : 1.1, 0.2], `T${tier}`);
assert.deepEqual(yak.findBuff(`ironmn:crab:${c.id}`)?.mods, { physTakenMul: 0.8 }, `T${tier}: 角峰 behind it`);
// damage taken by 角峰: physical ×0.8, arts unchanged
const p = h.b.dealDamage(null, yak, { amount: 1000, type: 'phys', canDodge: false, sourceless: true });
const ar = h.b.dealDamage(null, yak, { amount: 1000, type: 'arts', canDodge: false, sourceless: true });
approx(p, Math.max(1000 - yak.s.def, 50) * 0.8, `T${tier}: physical ×0.8`, 1e-3);
approx(ar, 1000 * (1 - yak.s.res / 100), `T${tier}: arts ×1`, 1e-3);
yak.hp = yak.s.maxHp;
// its skill: an enemy in the 3-13, SP full
const main = h.spawn('enemy_fly', { pos: [10, 7] });
const near = h.spawn('enemy_dummy', { pos: [10, 8] });
const far = h.spawn('enemy_dummy', { pos: [12, 8] });
c.skill.gainSp(99);
const atk = u.s.atk;
const hp0 = c.hp;
assert.ok(h.runUntil(() => c.skill.activations === 1, 1), `T${tier}: cast at full SP with an enemy in range`);
approx(hp0 - c.hp, c.s.maxHp * 0.025, `T${tier}: −2.5 % max HP`);
h.run(1);
const by = (t) => h.hooksOf('damaged').filter((x) => x.source === c && x.target === t);
assert.equal(by(main).length, 1, `T${tier}: the flyer is the first target (可对空)`);
approx(by(main)[0].amount, atk * v.skill.bb.atk_scale, `T${tier}: ${v.skill.bb.atk_scale * 100} % of 白铁's ATK`);
assert.equal(by(near).length, 1, `T${tier}: the enemy within 1.1`);
approx(by(near)[0].amount, atk * v.skill.bb.atk_scale_2, `T${tier}: ${v.skill.bb.atk_scale_2 * 100} %`);
assert.equal(by(far).length, 0, `T${tier}: farther than 1.1 untouched`);
assert.deepEqual([by(main)[0].type, by(main)[0].dmg.isSkill], ['phys', true]);
done(h);
}
});
test('CRA-X: stage 3 节约经费 "当白铁周围8格存在自身装置时技力回复速度+0.2/秒" (one effect) — none at stage 1, none with the device outside his x-4', () => {
for (const [tier, mod, want] of [[5, X, 0], [6, X, 0.2], [6, Y, 0], [6, null, 0]]) {
const f = label([tier, true, mod]);
const { h, u, devs } = field({ tier, elite: true, mod, skill: 0, pieces: [[11, 6], [9, 4]] });
h.step();
assert.equal(u.s.spRecovery, u.base.spRecovery + want, `${f}: SP recovery`);
if (want) {
assert.deepEqual(u.findBuff('talent:ironmn:spRecover').mods, { spRecoveryFlat: 0.2 });
for (const d of devs) h.b.retreat(d, { reason: 'expired' });
h.step();
assert.equal(u.s.spRecovery, u.base.spRecovery, `${f}: none once no device stands on his x-4`);
}
done(h);
}
});
test('CRA-Y: stage 3 "自身装置天赋生效的干员攻击速度+6(此效果不受技能影响)" — the operator a 平台 faces / the one behind a 铁钳号, +6 also while S1 runs; none at stage 1', () => {
for (const [tier, want] of [[5, 0], [6, 6]]) {
for (const skill of [0, 1, 2]) {
const f = `T${tier} CRA-Y S${skill + 1}`;
const piece = skill === 2 ? [10, 7] : [11, 4];
const op = skill === 2 ? { uid: 3, chessId: 'chess_char_1_08_a', row: 10, col: 6 } : { uid: 3, chessId: 'chess_char_1_08_a', row: 11, col: 5 };
const { h, devs } = field({ tier, elite: true, mod: Y, skill, pieces: [piece], others: [op] });
const tx = h.unit(3);
h.step();
assert.equal(tx.s.aspd, tx.base.aspd + want, `${f}: ASPD`);
if (want) assert.deepEqual(tx.findBuff(`ironmn:aspd:${devs[0].id}`).mods, { aspd: 6 });
done(h);
}
}
const { h, u } = field({ tier: 6, elite: true, mod: Y, skill: 0, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_08_a', row: 11, col: 5 }] });
const tx = h.unit(3);
u.skill.gainSp(999);
h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3));
h.step();
assert.equal(tx.s.aspd, tx.base.aspd + 6, 'not scaled by S1');
done(h);
});
test('铁钳号·原型机: enemies never select it (a ranged enemy beside it shoots 白铁 instead); its own HP loss can knock it out — then 节约经费 rolls and it returns like any device (20 s, CRA-Y 10 s; 10 DP, CRA-X 6)', () => {
const shooter = enemyRec({ key: 'enemy_shooter', hp: 1e9, speed: 0, mass: 0, range: 2.5, atk: 50, bat: 1 });
for (const [tier, elite, mod] of [[5, false, null], [6, true, X], [6, true, Y]]) {
const f = label([tier, elite, mod]);
const units = [
{ uid: 1, diy: { slot: SLOT[tier], charId: IRON, skillIndex: 2, uniEquipId: mod }, elite, row: 10, col: 5 },
{ uid: 10, kind: 'token', tokenId: CRAB, ownerUid: 1, row: 11, col: 6 },
];
const h = makeBattle({ defs: { enemies: { ...ENEMIES, enemy_shooter: shooter } }, timeLimit: 900, autoFinish: false, seed: 7, flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'death'], captureNoisy: true, units });
h.step();
h.b.players[0].dp = 99;
const u = h.unit(1);
const c = h.b.allyUnits.find((a) => a.defId === CRAB);
const e = h.spawn('enemy_shooter', { pos: [9, 7] });
h.run(5);
const atk = h.events.filter((x) => x[0] === 'atk' && x[1] === e.id);
assert.ok(atk.length > 0 && atk.every((x) => x[2] !== c.id), `${f}: the enemy shoots, never at the crab`);
assert.equal(c.hp, c.s.maxHp, `${f}: untouched`);
// knocked out by its own HP loss
h.b.kill(e, null);
h.step();
const v = tokOf(CRAB, tier, elite, 2, mod);
c.hp = 10;
c.skill.gainSp(99);
const asked = forceChance(h, true);
h.spawn('enemy_dummy', { pos: [11, 8] });
assert.ok(h.runUntil(() => !c.alive, 2), `${f}: its −2.5 % knocks it out`);
assert.deepEqual(asked.length, 1, `${f}: 节约经费 rolled (on his x-4)`);
const t0 = h.b.time;
assert.ok(h.runUntil(() => c.alive, v.stats.respawnTime + 2), `${f}: back`);
approx(h.b.time - t0, v.stats.respawnTime, `${f}: after ${v.stats.respawnTime} s`, 0.3);
approx(h.b.players[0].dp, 99 - v.stats.cost, `${f}: −${v.stats.cost} DP`);
assert.equal(c.hp, c.s.maxHp, `${f}: at full HP`);
void u;
done(h);
}
});
+299
View File
@@ -0,0 +1,299 @@
// test/content/op_nasti.test.js — the 自选 operator kit of 娜斯提 (char_4212_nasti, 6★ 工匠; kit
// server/sim/content/kits/ops/op-nasti.js) and of her <支援装置> “质检专员” (token_10059_nasti_nstdef), “监工专员”
// (token_10060_nasti_nstchr) and “应急承重小组” (token_10061_nasti_nstbld: the 小工程师 and its 高台), fielded the production way (a
// DIY slot + its `diy` pick; the devices as the placed hand pieces of her player) in every form: tiers 5 / 6, normal (E2 Lv1,
// skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or CRA-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_nasti.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 NASTI = 'char_4212_nasti';
const DEF = 'token_10059_nasti_nstdef', CHR = 'token_10060_nasti_nstchr', BLD = 'token_10061_nasti_nstbld';
const TOK = [DEF, CHR, BLD];
const FORMS = BACKUPS.units[NASTI].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_nasti';
const S1 = 'skchr_nasti_1', S2 = 'skchr_nasti_2', S3 = 'skchr_nasti_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);
const tokOf = (id, tier, elite, skill, mod) => {
let v = BACKUPS.tokens[id].variants[`${NASTI}@${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_rng: dummy('enemy_rng', { range: 2 }),
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }),
enemy_hit: enemyRec({ key: 'enemy_hit', hp: 1e9, speed: 0.6, mass: 0, atk: 400, bat: 1 }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], [5, true, null], [5, true, X], [6, true, null], [6, true, X]];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, dir = 'RIGHT', pieces = [], others = [], seed = 5, dp = 99 } = {}) {
const units = [
{ uid: 1, diy: { slot: SLOT[tier], charId: NASTI, skillIndex: skill, uniEquipId: mod }, elite, row, col, dir },
...pieces.map(([r, c, d], i) => ({ uid: 10 + i, kind: 'token', tokenId: TOK[skill], ownerUid: 1, row: r, col: c, dir: d ?? 'RIGHT' })),
...others,
];
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'death', 'deploy', 'spGain', 'heal'], captureNoisy: true,
units,
});
h.step();
h.b.players[0].dp = dp;
const u = h.unit(1);
const devs = h.b.allyUnits.filter((a) => a.kind === 'token' && a.defId === TOK[skill] && a.uid != null).sort((a, b) => a.uid - b.uid);
return { h, u, devs };
}
const st = (u) => u.mem.craftsman;
const platformOf = (u) => u.mem.nastiPlatform ?? null;
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const protectOf = (a) => a.findBuff('protect')?.mods ?? null;
test('娜斯提 in every 自选 form: her kit (all three skills authored), stats + module attributes, 1-1, blocks 2, melee physical ground-only, 协防 (no faction bond), no 特质, the data triggers; offered as a pick', () => {
assert.equal(OPERATOR_KITS[NASTI], KITS[NASTI]);
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], [NASTI, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [2, 'melee', false, 'phys', 'craftsman', 1.5], `${label(f)}: 工匠`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
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', 'DEFAULT', 'DEFAULT']);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr], [{ maxHp: 78, atk: 26, def: 25 }, { maxHp: 145, atk: 40, def: 45 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(NASTI));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(NASTI), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: NASTI, skillIndex: 1, uniEquipId: X } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: NASTI, skillIndex: 1, uniEquipId: X } } });
});
test('<支援装置> hand pieces: the picked skill\'s device (variant sources), 2 of them (deploy limit 2, "最多可部署2个"); CRA-X costs 2 / 2 / 3; stock 3 (CRA-X 4); CRA-X stage 3: the first device deployment after each of hers costs 3 less', () => {
for (const [skill, id] of TOK.entries()) {
for (const s of [0, 1, 2]) assert.equal(tokOf(id, 6, true, s, null).sources.includes('skill'), s === skill, `${id} under S${s + 1}`);
assert.equal(tokOf(id, 6, true, skill, null).stats.deployLimit, 2, `${id}: 2 hand pieces`);
}
assert.deepEqual(TOK.map((id, s) => tokOf(id, 6, true, s, null).stats.cost), [4, 4, 6]);
assert.deepEqual(TOK.map((id, s) => tokOf(id, 6, true, s, X).stats.cost), [2, 2, 3]);
assert.deepEqual(TOK.map((id, s) => tokOf(id, 6, true, s, null).stats.respawnTime), [8, 4, 6]);
for (const [tier, elite, mod, cap, cut] of [[5, false, null, 3, 0], [5, true, X, 4, 0], [6, true, X, 4, 3]]) {
const f = label([tier, elite, mod]);
const { h, u, devs } = field({ tier, elite, mod, skill: 0, pieces: [[11, 7], [9, 7]] });
const [a, b] = devs;
assert.deepEqual([st(u).stock, st(u).cap, a.alive, b.alive], [cap, cap, true, true], `${f}: stock, both deployed free`);
assert.equal(u.mem.nastiFirst, false, `${f}: the battle-start deployment used the first-device cut`);
// she leaves and comes back: the first device back costs `cut` less, the next one its full cost
h.b.kill(u, null);
assert.ok(!a.alive && !b.alive, `${f}: her devices leave with her`);
h.b.players[0].dp = 999;
assert.ok(h.runUntil(() => u.alive, 200), `${f}: back`);
h.b.players[0].dp = 100;
assert.ok(h.runUntil(() => a.alive && b.alive, 20), `${f}: both devices back`);
approx(h.b.players[0].dp, 100 - Math.max(0, a.base.cost - cut) - a.base.cost, `${f}: the first back ${Math.max(0, a.base.cost - cut)} DP, the second ${a.base.cost}`);
done(h);
}
});
test('注意安全: 10 % 庇护 on her; once a 远程 enemy is on the field (until she leaves) 15 % on her and on our 远程 operators (not the 近战 ones), and +1 SP every 6 s for them', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const t1 = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
assert.deepEqual([t1['nasti_t2[res].damage_resistance'], t1['nasti_t2[res_plus].damage_resistance'], t1['nasti_t2[add_sp].interval']], [0.1, 0.15, 6]);
const { h, u } = field({ tier, elite, skill: 0, others: [{ uid: 3, chessId: 'chess_char_1_14_a', row: 9, col: 3 }, { uid: 4, chessId: 'chess_char_1_08_a', row: 12, col: 3 }] });
const prov = h.unit(3), tx = h.unit(4);
h.run(0.5);
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 0.9, artsTakenMul: 0.9 }, null, null], `T${tier}: 10 % on her only`);
h.spawn('enemy_dummy', { pos: [11, 9] });
h.run(0.5);
assert.deepEqual(protectOf(u), { physTakenMul: 0.9, artsTakenMul: 0.9 }, `T${tier}: a 近战 enemy changes nothing`);
const rng = h.spawn('enemy_rng', { pos: [12, 9] });
h.run(0.5);
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 0.85, artsTakenMul: 0.85 }, { physTakenMul: 0.85, artsTakenMul: 0.85 }, null], `T${tier}: 15 % on her and 格雷伊 (远程), not 德克萨斯`);
const gifts = (a) => h.hooksOf('spGain').filter((c) => c.unit === a && c.reason === 'talent').map((c) => c.t);
prov.skill.sp = 0;
u.skill.sp = 0;
const g0 = gifts(prov).length;
h.run(12.1);
assert.equal(gifts(prov).length - g0, 2, `T${tier}: +1 SP every 6 s (格雷伊)`);
assert.ok(gifts(u).length >= 2, `T${tier}: and her`);
assert.equal(gifts(tx).length, 0, `T${tier}: not 德克萨斯`);
h.b.kill(rng, null);
h.run(1);
assert.deepEqual(protectOf(prov), { physTakenMul: 0.85, artsTakenMul: 0.85 }, `T${tier}: permanent until she leaves`);
h.b.kill(u, null);
h.run(1);
assert.equal(protectOf(prov), null, `T${tier}: gone with her`);
done(h);
}
});
test('S1 “拱卫” + “质检专员”: DEF +25 % on the operator it faces; while S1 runs DEF +40 / 46 %, block +1 and the device −2 % max HP / s; S1 38 / 40 s, ATK / DEF +52 / 65 %, every blocked enemy at once, +1 stock at the start', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const v = tokOf(DEF, tier, elite, 0, null);
const { h, u, devs } = field({ tier, elite, skill: 0, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
const yak = h.unit(3);
const [d] = devs;
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.def, sk.bb['talent@def'], v.talents[1].bb.def, v.skill.bb['talent@block_cnt'], v.skill.bb['talent@hp_ratio']],
[elite ? 40 : 38, elite ? 0.65 : 0.52, elite ? 0.65 : 0.52, elite ? 0.46 : 0.4, 0.25, 1, 0.02], `T${tier}`);
h.step();
assert.deepEqual(yak.findBuff(`nasti:nstdef:${d.id}`)?.mods, { defPct: 0.25 }, `T${tier}: 严禁偷工减料`);
assert.ok(d.s.flags.untargetable && d.s.flags.invulnerable && d.s.flags.noHeal, `T${tier}: 不会受到攻击, 无敌, 禁疗`);
st(u).stock = 1;
u.skill.gainSp(999);
h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3));
assert.equal(st(u).stock, 2, `T${tier}: +1 stock`);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.02);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
assert.deepEqual(u.skill.spec.attack, { hitAllBlocked: true });
h.step();
assert.deepEqual(yak.findBuff(`nasti:nstdef:${d.id}`)?.mods, { defPct: sk.bb['talent@def'], blockCnt: 1 }, `T${tier}: the S1 mode`);
assert.equal(yak.s.blockCnt, yak.base.blockCnt + 1, `T${tier}: block +1`);
const hp0 = d.hp;
h.run(5.02);
approx(hp0 - d.hp, d.s.maxHp * 0.02 * 5, `T${tier}: −2 % / s`, 1e-3);
h.runUntil(() => !u.skill.active, sk.duration + 1);
h.step();
assert.deepEqual(yak.findBuff(`nasti:nstdef:${d.id}`)?.mods, { defPct: 0.25 }, `T${tier}: back to +25 %`);
approx(d.hp, d.s.maxHp * (1 - 0.02 * (sk.duration - 1)), `T${tier}: ${sk.duration - 1} ticks of −2 % (at 1 … ${sk.duration - 1} s; the skill ends at ${sk.duration})`, 1e-3);
done(h);
}
});
test('S2 “执行” + “监工专员”: deployed at 3 / 8 HP, +1 HP every 6 SP (阻回 at full HP); S2 4 s: no attack, a barrier of 6 / 8 % of her max HP each 0.5 s up to 48 / 64 %; the device: +1 SP and a 3 / 4 % barrier to the operator it faces each 0.5 s, −1 HP each, leaves at the end; +1 stock at the end; her barriers go when she leaves', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
const v = tokOf(CHR, tier, elite, 1, null);
const { h, u, devs } = field({ tier, elite, skill: 1, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
const yak = h.unit(3);
const [d] = devs;
assert.deepEqual([sk.duration, sk.bb['attack@nasti_s2[update_shield].shield_each_hp_rate'], sk.bb['attack@nasti_s2[update_shield].shield_max_hp_rate'], v.skill.bb['talent@shield_each_hp_rate'], v.skill.bb['talent@shield_max_hp_rate'], v.stats.maxHp, v.talents[1].bb.init_hp, v.skill.spCost],
[4, elite ? 0.08 : 0.06, elite ? 0.64 : 0.48, elite ? 0.04 : 0.03, elite ? 0.64 : 0.48, 8, 3, 6], `T${tier}`);
assert.deepEqual([d.hp, d.s.maxHp], [3, 8], `T${tier}: 3 / 8 HP`);
h.run(12.2);
assert.equal(d.hp, 5, `T${tier}: +1 HP every 6 s`);
h.run(18.2);
assert.equal(d.hp, 8, `T${tier}: full`);
assert.ok(d.s.flags.noSp, `T${tier}: 阻回 at full HP`);
// S2: 8 pulses over 4 s on 角峰; the device dies on its last HP; her own barrier
st(u).stock = 1;
yak.skill.sp = 0;
u.skill.gainSp(999);
h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
assert.deepEqual(u.skill.spec.attack, { noAttack: true }, `T${tier}: 停止攻击`);
const t0 = h.b.time;
h.runUntil(() => !u.skill.active, 6);
approx(h.b.time - t0, 4, `T${tier}: 4 s`, 0.03);
const each = u.s.maxHp * v.skill.bb['talent@shield_each_hp_rate'];
approx(yak.findBuff(`nasti:shield:${u.id}`)?.shield ?? 0, 8 * each, `T${tier}: 8 pulses of ${v.skill.bb['talent@shield_each_hp_rate'] * 100} % of her max HP`, 1e-3);
assert.equal(h.hooksOf('spGain').filter((c) => c.unit === yak && c.reason === 'device').length, 8, `T${tier}: 8 SP`);
approx(u.findBuff('nasti:s2shield').shield, u.s.maxHp * sk.bb['attack@nasti_s2[update_shield].shield_max_hp_rate'], `T${tier}: her barrier at the cap`, 1e-3);
assert.equal(d.alive, false, `T${tier}: the device spent (8 HP) / left at the end`);
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at the end (the device not back yet)`);
assert.ok(h.runUntil(() => d.alive, 10), `T${tier}: back after 4 s`);
assert.deepEqual([d.hp, st(u).stock], [3, 1]);
// she leaves: every barrier of her S2 goes
h.b.kill(u, null);
assert.ok(!yak.findBuff(`nasti:shield:${u.id}`) && !u.findBuff('nasti:s2shield'), `T${tier}: barriers gone with her`);
done(h);
}
});
test('S3 栖脚地 + 应急承重小组: the 小工程师 builds the 高台 on the free low tile it faces (none on a taken tile); 50 SP a level (its own 1 / s + 1 / s from the 小工程师, 2 / s in S3); level 1 attackable, block 1; level 2 +1000 HP / +100 DEF / +5 RES; level 3 +2000 / +200 / +10, 阻回; heals 1 % (2 %) for −2 %; stays when she leaves only at level 3; S3 15 s, ATK / DEF +100 / 130 %', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const v = tokOf(BLD, tier, elite, 2, null);
const { h, u, devs } = field({ tier, elite, skill: 2, pieces: [[11, 4], [9, 4, 'UP']], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 10, col: 4 }] });
const [eng, blocked] = devs;
const p = platformOf(u);
assert.ok(p && p.alive, `T${tier}: the 高台 built`);
assert.deepEqual([p.tileR, p.tileC, p.mem.nastiLevel], [11, 5, 0], `T${tier}: on the tile it faces`);
assert.equal(h.b.allyUnits.filter((a) => a.defId === BLD && a.mem.nastiPlatform).length, 1, `T${tier}: one 高台 (the other 小工程师 faces 角峰)`);
void blocked;
assert.ok(p.s.flags.untargetable && p.s.flags.invulnerable && p.s.blockCnt === 0, `T${tier}: 未激活 — untargetable, invulnerable, blocks nobody`);
assert.ok(eng.s.flags.untargetable && eng.s.flags.invulnerable && eng.s.flags.noHeal, `T${tier}: the 小工程师 不会受到攻击`);
assert.deepEqual([v.skill.spCost, v.talents[0].bb.m1_sp, v.talents[0].bb.m2_sp, v.talents[0].bb['nasti_nstbld_m1[heal_m456].hp_ratio'], v.talents[0].bb['nasti_nstbld_m2[heal_m456].hp_ratio']], [50, 1, 2, 0.01, 0.02], `T${tier}`);
// level 1 after 25 s (1 + 1 SP / s)
assert.ok(h.runUntil(() => p.mem.nastiLevel === 1, 27), `T${tier}: level 1`);
approx(h.b.time, 25, `T${tier}: 50 SP at 2 / s`, 0.08);
assert.ok(!p.s.flags.untargetable && !p.s.flags.invulnerable && p.s.blockCnt === 1, `T${tier}: 激活 — attackable, block 1`);
// S3: 3 SP / s (1 own + 2), level 2
u.skill.gainSp(999);
h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK +${sk.bb.atk * 100} %`);
approx(u.skill.timeLeft, 15, `T${tier}: 15 s`, 0.05);
const sp0 = p.skill.sp, tS = h.b.time;
h.run(5);
approx(p.skill.sp - sp0, 3 * 5, `T${tier}: 3 SP / s in S3`, 0.05);
void tS;
assert.ok(h.runUntil(() => p.mem.nastiLevel === 2, 20), `T${tier}: level 2`);
approx(p.s.maxHp, v.stats.maxHp + 1000, `T${tier}: +1000 HP`);
assert.deepEqual([p.s.def, p.s.res], [v.stats.def + 100, v.stats.res + 5], `T${tier}: +100 DEF / +5 RES`);
// heal: 1 % of its max HP for −2 % of the 小工程师's
h.runUntil(() => !u.skill.active, 20);
p.hp = p.s.maxHp * 0.5;
const e0 = eng.hp, p0 = p.hp;
h.run(1.01);
approx(p.hp - p0, p.s.maxHp * 0.01, `T${tier}: heals 1 %`, 0.02);
approx(e0 - eng.hp, eng.s.maxHp * 0.02, `T${tier}: −2 % own`, 0.02);
// level 3: 阻回; stays when she leaves
p.skill.gainSp(999);
h.step(2);
assert.equal(p.mem.nastiLevel, 3, `T${tier}: level 3`);
approx(p.s.maxHp, v.stats.maxHp + 2000, `T${tier}: +2000 HP`);
assert.ok(p.s.flags.noSp, `T${tier}: 阻回 at the top level`);
h.b.kill(u, null);
assert.ok(p.alive, `T${tier}: a level-3 高台 stays when she leaves`);
assert.ok(!eng.alive, `T${tier}: the 小工程师 goes with her`);
done(h);
}
// below level 3 the 高台 goes with her; no 高台 on a taken tile
const { h, u } = field({ tier: 6, elite: true, skill: 2, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
assert.equal(platformOf(u), null, 'the tile it faces is taken: no 高台');
done(h);
const r2 = field({ tier: 6, elite: true, skill: 2, pieces: [[11, 4]] });
const p2 = platformOf(r2.u);
r2.h.b.kill(r2.u, null);
assert.ok(!p2.alive, 'a level-0 高台 goes with her');
done(r2.h);
});
test('CRA-X stage 3 "装置可与攻击范围内其他装置共同提供效果": a 质检专员 with another on its range also reaches that one\'s range (the operator ahead gets both) — not at stage 1 or without the module', () => {
for (const [tier, mod, want] of [[6, X, 2], [5, X, 1], [6, null, 1]]) {
const { h, devs } = field({ tier, elite: true, mod, skill: 0, pieces: [[11, 3], [11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
const yak = h.unit(3);
h.step();
const n = devs.filter((d) => yak.findBuff(`nasti:nstdef:${d.id}`)).length;
assert.equal(n, want, `T${tier} ${mod ?? 'none'}: ${want} device(s) reach 角峰`);
approx(yak.s.def, yak.base.def * (1 + 0.25 * want), `T${tier}: DEF +${25 * want} %`);
done(h);
}
});
+390
View File
@@ -0,0 +1,390 @@
// test/content/op_necras.test.js — the 自选 operator kit of 死芒 (char_450_necras, 6★ 塑灵术师; kit
// server/sim/content/kits/ops/op-necras.js) and of her summon 悲叹的仆役 (token_10043_necras_skeltn, and its special form),
// fielded the production way (a DIY slot + its `diy` pick) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module)
// and elite (E2 Lv60, rank 7) with no module or SOC-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_necras.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 NEC = 'char_450_necras';
const SKEL = 'token_10043_necras_skeltn';
const FORMS = BACKUPS.units[NEC].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_necras';
const S1 = 'skchr_necras_1', S2 = 'skchr_necras_2', S3 = 'skchr_necras_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);
const skelOf = (tier, elite, skill) => {
let v = BACKUPS.tokens[SKEL].variants[`${NEC}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
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 ENEMIES = {
enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e9, speed: 0, mass: 0 }),
enemy_weak: enemyRec({ key: 'enemy_weak', hp: 100, speed: 0, mass: 0 }),
enemy_fly: enemyRec({ key: 'enemy_fly', hp: 1e9, speed: 0, mass: 0, motion: 'FLY' }),
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.5, mass: 0 }),
};
const FORMS_ALL = [[5, false, null], [6, false, null], [5, true, null], [5, true, X], [6, true, null], [6, true, X]];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 死芒 as uid 1 at (10, 3) facing RIGHT: her 3-1 is rows 9–11 × cols 3–5 and (10, 6). */
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 3, others = [], seed = 5 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'death', 'deploy', 'statusApplied', 'heal'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: NEC, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
const skels = (u) => (u.mem.necras?.skels ?? []).filter((s) => s.alive);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
/** Knock an enemy out at `pos` (the kill is credited to nobody: anyone's kill counts). */
function killAt(h, pos, key = 'enemy_weak') {
const e = h.spawn(key, { pos });
h.step();
h.b.kill(e, null);
return e;
}
test('死芒 in every 自选 form: her kit (all three skills authored), stats + module attributes, 3-1, blocks 1, ranged arts that hit air, 维多利亚, no 特质, the data triggers (S3 SKILL_RANGE on its 3-3); offered as a pick', () => {
assert.equal(OPERATOR_KITS[NEC], KITS[NEC]);
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], [NEC, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [1, 'ranged', true, 'arts', 1.6], `${label(f)}: 塑灵术师`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
done(h);
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'SKILL_RANGE']);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr], [{ maxHp: 120, atk: 35 }, { maxHp: 200, atk: 55 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(NEC));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(NEC), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: NEC, skillIndex: 2, uniEquipId: X } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: NEC, skillIndex: 2, uniEquipId: X } } });
});
test('复燃: a knock-out on her range (anyone\'s, stealth ignored) makes a 悲叹的仆役 on its tile 0.1 s later (facing as she does); off her range nothing; up to 3, then the upgrade (+100 % HP / ATK, +40 % DEF, block +1, healed to full; not yet upgraded first), all upgraded: the lowest healed; they leave with her', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const t0 = formOf(tier, elite).talents.find((t) => t.index === 0).bb;
const v = skelOf(tier, elite, 0);
assert.deepEqual([t0.max_token_cnt, t0.max_hp, t0.atk, t0.def, t0.block_cnt], [3, 1, 1, 0.4, 1], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 1 });
killAt(h, [10, 5]);
assert.equal(skels(u).length, 0, `T${tier}: not at once`);
h.run(0.11);
const [a] = skels(u);
assert.ok(a, `T${tier}: one 0.1 s later`);
assert.deepEqual([a.tileR, a.tileC, a.dir, a.defId], [10, 5, u.dir, SKEL], `T${tier}: on the kill tile, facing as she does`);
assert.deepEqual([a.base.maxHp, a.base.atk, a.base.def, a.s.blockCnt, a.profile.dmgType, a.profile.attack, a.profile.canHitFly], [v.stats.maxHp, v.stats.atk, v.stats.def, 1, 'arts', 'melee', false], `T${tier}: the token's stats, arts melee, ground only`);
assert.deepEqual(a.liveRangeGrid, v.rangeGrid, `T${tier}: x-5`);
assert.ok(a.s.flags.noHeal && a.s.flags.noSp && a.s.flags.silence, `T${tier}: 禁疗, 阻回, 沉默`);
killAt(h, [10, 8]);
h.run(0.2);
assert.equal(skels(u).length, 1, `T${tier}: off her range — nothing`);
killAt(h, [9, 4]);
h.run(0.2);
killAt(h, [11, 4]);
h.run(0.2);
assert.equal(skels(u).length, 3, `T${tier}: three`);
const [, b, c] = skels(u);
b.hp = b.s.maxHp * 0.5;
c.hp = c.s.maxHp * 0.3;
killAt(h, [10, 4]);
h.run(0.2);
assert.equal(skels(u).length, 3, `T${tier}: no 4th`);
assert.ok(c.findBuff('necras:upgrade') && !a.findBuff('necras:upgrade') && !b.findBuff('necras:upgrade'), `T${tier}: the lowest-HP one upgraded`);
assert.deepEqual(c.findBuff('necras:upgrade').mods, { hpPct: 1, atkPct: 1, defPct: 0.4, blockCnt: 1 });
assert.deepEqual([c.hp, c.s.maxHp, c.s.blockCnt], [c.base.maxHp * 2, c.base.maxHp * 2, 2], `T${tier}: healed to full, block 2`);
killAt(h, [10, 4]);
h.run(0.2);
killAt(h, [10, 4]);
h.run(0.2);
assert.ok(skels(u).every((s) => s.findBuff('necras:upgrade')), `T${tier}: all three upgraded`);
b.hp = 100;
killAt(h, [10, 4]);
h.run(0.2);
assert.equal(b.hp, b.s.maxHp, `T${tier}: all upgraded — the lowest healed`);
h.b.kill(u, null);
assert.equal(skels(u).length, 0, `T${tier}: they leave with her`);
done(h);
}
});
test('复燃: no deployable kill tile ⇒ a random free low melee tile of her range, those with an enemy first; no tile at all ⇒ the upgrade instead', () => {
const { h, u } = field({ tier: 6, elite: true, skill: 1, others: [
{ uid: 3, chessId: 'chess_char_1_02_a', row: 9, col: 3 }, { uid: 4, chessId: 'chess_char_1_02_a', row: 9, col: 4 }, { uid: 5, chessId: 'chess_char_1_02_a', row: 9, col: 5 },
{ uid: 6, chessId: 'chess_char_1_02_a', row: 11, col: 3 }, { uid: 7, chessId: 'chess_char_1_02_a', row: 11, col: 4 }, { uid: 8, chessId: 'chess_char_1_02_a', row: 11, col: 5 },
] });
// her range: (9|10|11, 3–5) + (10, 6); free: (10, 4), (10, 5), (10, 6); an enemy stands on (10, 6)
h.spawn('enemy_dummy', { pos: [10, 6] });
// a kill on a taken tile (an operator on it) ⇒ the tile with the enemy
killAt(h, [9, 4]);
h.run(0.2);
const [a] = skels(u);
assert.deepEqual([a.tileR, a.tileC], [10, 6], 'the free tile with an enemy');
killAt(h, [9, 5]);
h.run(0.2);
killAt(h, [9, 5]);
h.run(0.2);
assert.deepEqual(skels(u).map((s) => [s.tileR, s.tileC]).sort(), [[10, 4], [10, 5], [10, 6]], 'then the other free tiles');
done(h);
});
test('回光黯淡: her and her summons\' damage on an enemy below 50 % HP ×1.4 (the final damage); SOC-X "攻击力提升至115%" on an enemy her summon blocks (stage 1 and 3), which she can attack anywhere', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, X], [6, true, X]]) {
const f = label([tier, elite, mod]);
const { h, u } = field({ tier, elite, mod, skill: 1, row: 9, col: 3 });
const e = h.spawn('enemy_dummy', { pos: [9, 5] });
h.run(3);
const hits = () => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.dmg.isAttack);
const full = hits().at(-1).amount;
approx(full, u.s.atk, `${f}: ×1 at full HP`);
e.hp = e.s.maxHp * 0.49;
const n0 = hits().length;
h.run(2);
approx(hits().slice(n0).at(-1).amount, u.s.atk * 1.4, `${f}: ×1.4 below 50 %`);
done(h);
}
// the trait: she attacks the enemy her summon blocks out of her range (×1.15 with SOC-X)
for (const [tier, elite, mod, mul] of [[5, false, null, 1], [5, true, X, 1.15], [6, true, X, 1.15]]) {
const f = label([tier, elite, mod]);
const { h, u } = field({ tier, elite, mod, skill: 1, row: 12, col: 8 });
// her range: rows 11–13 × cols 8–10 and (12, 11); a 悲叹的仆役 made on (11, 9) blocks a walker on row 11 coming from the right
killAt(h, [11, 9]);
h.run(0.2);
const [s] = skels(u);
assert.deepEqual([s.tileR, s.tileC], [11, 9]);
const w = h.spawn('enemy_walk', { pos: [11, 13] });
assert.ok(h.runUntil(() => w.blockedBy === s, 20), `${f}: blocked by her summon`);
h.step(3);
assert.ok(u.extraRangeKeys && u.extraRangeKeys.includes(Math.round(w.y) * 21 + Math.round(w.x)), `${f}: its tile is her target`);
const n0 = h.hooksOf('damaged').filter((c) => c.source === u && c.target === w).length;
h.run(4);
const hit = h.hooksOf('damaged').filter((c) => c.source === u && c.target === w && c.dmg.isAttack).slice(n0);
assert.ok(hit.length >= 1, `${f}: she attacks it`);
approx(hit[0].amount, u.s.atk * mul, `${f}: ×${mul}`);
done(h);
}
});
test('SOC-X stage 3: a knock-out on her range or her summons\' ranges ⇒ her ATK +25 % for 10 s (refreshed); one on a summon\'s range only also makes a 悲叹的仆役 (a random tile of her range) — none of it at stage 1', () => {
for (const [tier, mod, on] of [[5, X, false], [6, X, true], [6, null, false]]) {
const f = label([tier, true, mod]);
const { h, u } = field({ tier, elite: true, mod, skill: 1, row: 10, col: 3 });
// a summon on the tip of her range (10, 6): its x-5 reaches (10, 7), (9, 6), (11, 6) — outside hers
killAt(h, [10, 6]);
h.run(0.2);
const [s] = skels(u);
assert.deepEqual([s.tileR, s.tileC], [10, 6]);
assert.equal(!!u.findBuff('necras:killAtk'), on, `${f}: ATK buff`);
if (on) {
assert.deepEqual(u.findBuff('necras:killAtk').mods, { atkPct: 0.25 });
approx(u.findBuff('necras:killAtk').timeLeft, 10 - 0.2, `${f}: 10 s`, 0.02);
h.run(5);
killAt(h, [9, 4]);
approx(u.findBuff('necras:killAtk').timeLeft, 10, `${f}: refreshed`, 0.02);
h.run(0.2);
}
const n = skels(u).length;
killAt(h, [10, 7]);
h.run(0.2);
assert.equal(skels(u).length, on ? n + 1 : n, `${f}: the summon-range kill makes one (stage 3 only)`);
done(h);
}
});
test('S1 噩愿: each 悲叹的仆役 made or upgraded strikes every enemy within 1.5 for her ATK × 310 % / 370 % arts (air units too); the cast re-makes them all — 1 per summon, 2 per upgraded one, 1 with none', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.bb.atk_scale, sk.bb.range_radius], [elite ? 3.7 : 3.1, 1.5]);
const { h, u } = field({ tier, elite, skill: 0 });
const near = h.spawn('enemy_fly', { pos: [11, 5] });
const far = h.spawn('enemy_dummy', { pos: [12, 7] });
killAt(h, [10, 5]);
h.run(0.2);
const by = (t) => h.hooksOf('damaged').filter((c) => c.source === u && c.target === t && c.dmg.tags?.includes('necras:s1'));
assert.equal(by(near).length, 1, `T${tier}: the flyer within 1.5`);
approx(by(near)[0].amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`);
assert.equal(by(far).length, 0, `T${tier}: farther untouched`);
// fill to 3 and upgrade one (another strike)
killAt(h, [10, 4]);
h.run(0.2);
killAt(h, [11, 4]);
h.run(0.2);
const k0 = by(near).length;
killAt(h, [10, 4]);
h.run(0.2);
assert.ok(skels(u).some((s) => s.findBuff('necras:upgrade')));
assert.ok(by(near).length >= k0, `T${tier}: the upgrade strikes too`);
// the cast: 2 + 1 + 1 = 4 credits ⇒ 3 summons + 1 upgrade
const old = skels(u).map((s) => s.id);
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `T${tier}: cast`);
h.run(0.5);
const now = skels(u);
assert.equal(now.length, 3, `T${tier}: three again`);
assert.ok(now.every((s) => !old.includes(s.id)), `T${tier}: all re-made`);
assert.equal(now.filter((s) => s.findBuff('necras:upgrade')).length, 1, `T${tier}: the 4th credit upgrades one`);
done(h);
}
// none on the field ⇒ one
const { h, u } = field({ tier: 6, elite: true, skill: 0 });
h.spawn('enemy_dummy', { pos: [10, 5] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === 1, 4));
h.run(0.3);
assert.equal(skels(u).length, 1, 'none ⇒ one');
done(h);
});
test('S2 折朽: 12 s; up to 2 enemies of her range sleep while linked and take her ATK × 100 % / 130 % arts every 0.5 s through it; no attack meanwhile; a linked one knocked out ⇒ +2 summons; every link broken ⇒ the skill ends and the sleep with it', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.duration, sk.bb.atk_scale, sk.bb.max_target, sk.bb.interval, sk.bb.additional_token_cnt], [12, elite ? 1.3 : 1, 2, 0.5, 2], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 1 });
const a = h.spawn('enemy_dummy', { pos: [10, 5] });
const b = h.spawn('enemy_dummy', { pos: [9, 5] });
const c = h.spawn('enemy_dummy', { pos: [11, 5] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
const slept = [a, b, c].filter((e) => e.findBuff('sleep'));
assert.equal(slept.length, 2, `T${tier}: two asleep`);
assert.deepEqual(u.skill.spec.attack, { noAttack: true });
const t0 = h.b.time;
h.run(3.01);
const ticks = h.hooksOf('damaged').filter((x) => x.source === u && slept.includes(x.target) && x.dmg.tags?.includes('necras:s2') && x.t > t0 - 1e-9);
assert.equal(ticks.length, 12, `T${tier}: 6 ticks each in 3 s`);
for (const x of ticks) approx(x.amount, u.s.atk * sk.bb.atk_scale * (1 - x.target.s.res / 100), `T${tier}: ${sk.bb.atk_scale * 100} % arts`, 1e-3);
assert.ok(ticks.every((x) => x.target.findBuff('sleep')), `T${tier}: through the sleep`);
// a linked one knocked out ⇒ +2 summons (and 复燃's own one on its tile)
const n0 = skels(u).length;
h.b.kill(slept[0], null);
h.run(0.4);
assert.equal(skels(u).length, n0 + 3, `T${tier}: 2 + 1 summons`);
assert.ok(u.skill.active, `T${tier}: one link left`);
h.b.kill(slept[1], null);
h.step(3);
assert.equal(u.skill.active, false, `T${tier}: every link broken ⇒ ends`);
done(h);
}
// the end releases the sleep; a stun interrupts it
const { h, u } = field({ tier: 6, elite: true, skill: 1 });
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 3));
assert.ok(e.findBuff('sleep'));
h.b.applyStatus(u, 'stun', { duration: 1 });
h.step(3);
assert.ok(!u.skill.active && !e.findBuff('sleep'), 'stunned: interrupted, the sleep gone');
done(h);
});
test('S3 冠死以冕: the next 悲叹的仆役 is the special form (+200 % HP / ATK, +100 % DEF, +10 RES, +0.7 s, block +1; 怨火缠身 15 SP / 8 s, 120 % ATK / s on its 1-1); the cast (SKILL_RANGE, 3-3 meanwhile): 2 strikes at her attack interval on every enemy of the 3-3 (550 % / 650 %), each consuming a normal summon to upgrade it (twice for an upgraded one; +80 / 80 / 40 %, heal 20 %, 3rd: block +1, 攻击距离 +1)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const v = skelOf(tier, elite, 2);
const dr = formOf(tier, elite).talents.find((t) => t.index === 3).bb;
assert.deepEqual([dr.max_hp, dr.atk, dr.def, dr.magic_resistance, dr.base_attack_time, dr.block_cnt], [2, 2, 1, 10, 0.7, 1]);
assert.deepEqual([sk.bb['attack@atk_scale'], sk.bb['attack@max_hp'], sk.bb['attack@atk'], sk.bb['attack@def'], sk.bb['attack@hp_ratio'], sk.bb['attack@max_stack_cnt'], sk.rangeId], [elite ? 6.5 : 5.5, 0.8, 0.8, 0.4, 0.2, 6, '3-3'], `T${tier}`);
assert.deepEqual([v.skill.skillId, v.skill.spCost, v.skill.bb['attack@atk_scale'], v.skill.bb['attack@duration']], ['sktok_necras_skeltn_3', 15, 1.2, 8]);
const { h, u } = field({ tier, elite, skill: 2 });
killAt(h, [10, 5]);
h.run(0.2);
const [d] = skels(u);
assert.ok(d.mem.necrasDragon, `T${tier}: the first one is the special form`);
assert.deepEqual([d.s.maxHp, d.s.atk, d.s.def, d.s.res, d.s.blockCnt], [d.base.maxHp * 3, d.base.atk * 3, d.base.def * 2, d.base.res + 10, 2], `T${tier}: the special form's attributes`);
approx(d.s.bat, d.base.bat + 0.7, `T${tier}: +0.7 s`);
assert.ok(!d.s.flags.noSp && d.skill.spCost === 15, `T${tier}: its skill charges`);
killAt(h, [10, 4]);
h.run(0.2);
killAt(h, [11, 4]);
h.run(0.2);
const [, n1, n2] = skels(u);
assert.ok(!n1.mem.necrasDragon && !n2.mem.necrasDragon && n1.s.flags.noSp, `T${tier}: the others normal, 阻回`);
// upgrade n1 (the earliest normal one), so its consumption upgrades twice
killAt(h, [10, 4]);
h.run(0.2);
assert.equal([n1, n2].filter((x) => x.findBuff('necras:upgrade')).length, 1, `T${tier}: one normal summon upgraded`);
const up1 = n1.findBuff('necras:upgrade') ? n1 : n2;
// the cast
const foe = h.spawn('enemy_dummy', { pos: [9, 6] });
const u0atk = u.s.atk;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 2), `T${tier}: SKILL_RANGE — an enemy in the 3-3`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-3 while it runs`);
const tCast = h.b.time;
assert.ok(h.runUntil(() => !u.skill.active, 5), `T${tier}: ends after its 2 strikes`);
const strikes = h.hooksOf('damaged').filter((x) => x.source === u && x.target === foe && x.dmg.tags?.includes('necras:s3'));
assert.equal(strikes.length, 2, `T${tier}: 2 strikes`);
approx(strikes[0].amount, u0atk * sk.bb['attack@atk_scale'] * (1 - foe.s.res / 100), `T${tier}: ${sk.bb['attack@atk_scale'] * 100} % arts`, 1e-3);
approx(strikes[1].t - strikes[0].t, u.s.interval, `T${tier}: at her attack interval`, 0.05);
void tCast;
h.run(0.3);
assert.ok(!n1.alive && !n2.alive, `T${tier}: both normal summons consumed`);
void up1;
assert.equal(d.mem.necrasUps, 3, `T${tier}: 2 (the upgraded one) + 1 upgrades`);
assert.deepEqual(d.findBuff('necras:dragonUp').mods, { hpPct: 0.8 * 3, atkPct: 0.8 * 3, defPct: 0.4 * 3, blockCnt: 1, rangeExtend: 1 }, `T${tier}: 3 upgrades — block +1, 攻击距离 +1`);
assert.equal(u.liveRangeGrid.length, formOf(tier, elite).rangeGrid.length, `T${tier}: her range back`);
done(h);
}
});
test('怨火缠身 (the special form\'s skill): 15 SP over time, 8 s, its ATK × 120 % arts every second on every enemy of its 1-1; the normal forms never cast (阻回)', () => {
const { h, u } = field({ tier: 6, elite: true, skill: 2 });
killAt(h, [10, 5]);
h.run(0.2);
const [d] = skels(u);
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => d.skill.active, 17), 'cast at 15 SP with an enemy in range');
approx(d.skill.timeLeft, 8, '8 s', 0.05);
const t0 = h.b.time;
h.runUntil(() => !d.skill.active, 10);
const ticks = h.hooksOf('damaged').filter((x) => x.source === d && x.target === e && x.dmg.isSkill && x.t >= t0 - 1e-9);
assert.equal(ticks.length, 8, 'one per second for 8 s');
approx(ticks[0].amount, d.s.atk * 1.2 * (1 - e.s.res / 100), '120 % arts', 1e-3);
done(h);
});
test('复燃: a stealthed enemy knocked out on her range counts (天赋无视隐匿); with no free tile left the summon becomes the upgrade', () => {
const tiles = [[9, 3], [9, 4], [9, 5], [11, 3], [11, 4], [11, 5], [10, 4], [10, 6]];
const { h, u } = field({ tier: 6, elite: true, skill: 1, others: tiles.map(([r, c], i) => ({ uid: 3 + i, chessId: 'chess_char_1_02_a', row: r, col: c })) });
const e = h.spawn('enemy_weak', { pos: [10, 5] });
h.step();
h.b.addBuff(e, { key: 'test:stealth', flags: { stealth: true } });
h.step();
h.b.kill(e, null);
h.run(0.2);
const [s1] = skels(u);
assert.ok(s1, 'a summon for the stealthed one');
assert.deepEqual([s1.tileR, s1.tileC], [10, 5]);
killAt(h, [9, 4]);
h.run(0.2);
assert.equal(skels(u).length, 1, 'no free tile: no second summon');
assert.ok(s1.findBuff('necras:upgrade'), '… the upgrade instead');
done(h);
});
+417 -129
View File
@@ -5443,8 +5443,8 @@
"snaps": "c4749bd0 699d0248 17649b18 7ab3fc70 09e3e1e3 6258b044 7238ac54 9f57607a 65ba9fc3 0b8e90f3 add042bf e94c1af2 fdf621ea 0e16b2df 721d914c d0aaa024 f828a7ac 7da502ae c52b962c 070c0db9 2a07d5b3 eca02286 05f89efe"
},
"diy-127": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=phatm2/S1 6_diy2_b=phatm2/S2 6_diy1_b=phatm2/S3 6_diy2_b=phatm2/S1/002_phatm2 6_diy1_b=phatm2/S2/002_phatm2 6_diy2_b=phatm2/S3/002_phatm2 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"about": "5_diy1_a=ironmn/S1 5_diy2_a=ironmn/S2 5_diy1_a=ironmn/S3 5_diy2_b=ironmn/S1 5_diy1_b=ironmn/S2 5_diy2_b=ironmn/S3 6_diy1_b=phatm2/S1 6_diy2_b=phatm2/S2 6_diy1_b=phatm2/S3 6_diy2_b=phatm2/S1/002_phatm2 6_diy1_b=phatm2/S2/002_phatm2 6_diy2_b=phatm2/S3/002_phatm2",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m06",
"modeId": "mode_multi_normal",
@@ -5454,133 +5454,139 @@
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 30,
"killed": 35,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":30,"total":105,"perfect":false,"leaks":75,"leaksUncounted":0,"coins":0,"dmg":1853739,"bossDmg":0,"heal":0,"deaths":12,"layerGains":{}}},
"players": {"p1":{"killed":35,"total":105,"perfect":false,"leaks":70,"leaksUncounted":0,"coins":0,"dmg":2349501,"bossDmg":0,"heal":0,"deaths":9,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,7.11,1,3,0,4818,2121,0,144,3],
["p1",2,"op","chess_char_5_diy2_b",2,"none",2908,1,6,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,127,0,7994,10002,0,41,0],
["p1",5,"op","chess_char_6_diy1_b",0,"none",1111,1,1,8,375991,0,0,131,32],
["p1",6,"op","chess_char_6_diy2_b",1,"none",274.62,1,4,4,332693,836,0,164,1],
["p1",7,"op","chess_char_6_diy1_b",2,"none",1111,1,124,8,321763,1111,0,93,3],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_002_phatm2",1311,1,2,5,446608,0,0,131,32],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_002_phatm2",0,0,131,4,158830,2622,0,95,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_phatm2",474.62,1,135,1,193400,3458,0,53,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,118,0,3784,3813,0,30,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,136,0,7859,1510,0,152,2],
["p1","~1","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~2","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~3","token","token_10055_phatm2_mndclv",null,null,3,0,28,0,0,0,0,0,null],
["p1","~4","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~5","token","token_10055_phatm2_mndclv",null,null,3,0,32,0,0,0,0,0,null],
["p1",1,"op","chess_char_5_diy1_a",0,null,438.15,1,3,2,10075,1604,0,101,3],
["p1",2,"op","chess_char_5_diy2_a",1,null,2042,1,6,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,2042,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,119,0,671,7479,0,24,0],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,7,0,15538,2493,0,129,3],
["p1",6,"op","chess_char_5_diy2_b",2,"none",2493,1,132,0,5014,4986,0,62,1],
["p1",7,"op","chess_char_6_diy1_b",0,"none",1111,1,1,5,258449,0,0,126,31],
["p1",8,"op","chess_char_6_diy2_b",1,"none",617.59,1,4,4,323680,493,0,161,1],
["p1",9,"op","chess_char_6_diy1_b",2,"none",274.62,1,5,5,557742,836,0,134,3],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_002_phatm2",474.62,1,2,7,424896,836,0,131,32],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_002_phatm2",474.62,1,128,6,289020,2147,0,111,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_phatm2",0,0,99,6,464417,2622,0,53,2],
["p1","~1","token","token_10055_phatm2_mndclv",null,null,3,0,28,0,0,0,0,0,null],
["p1","~2","token","token_10055_phatm2_mndclv",null,null,0,0,29,0,0,6,0,0,null],
["p1","~3","token","token_10055_phatm2_mndclv",null,null,0,0,30,0,0,4,0,0,null],
["p1","~4","token","token_10055_phatm2_mndclv",null,null,3,0,32,0,0,0,0,0,null],
["p1","~5","token","token_10055_phatm2_mndclv",null,null,1,0,39,0,0,2,0,0,null],
["p1","~6","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~7","token","token_10055_phatm2_mndclv",null,null,3,0,34,0,0,0,0,0,null],
["p1","~8","token","token_10055_phatm2_mndclv",null,null,3,0,38,0,0,0,0,0,null],
["p1","~9","token","token_10055_phatm2_mndclv",null,null,3,0,41,0,0,0,0,0,null],
["p1","~10","token","token_10055_phatm2_mndclv",null,null,3,0,75,0,0,0,0,0,null],
["p1","~7","token","token_10055_phatm2_mndclv",null,null,3,0,74,0,0,0,0,0,null],
["p1","~8","token","token_10055_phatm2_mndclv",null,null,3,0,75,0,0,0,0,0,null],
["p1","~9","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~10","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~11","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~12","token","token_10055_phatm2_mndclv",null,null,3,0,76,0,0,0,0,0,null],
["p1","~13","token","token_10055_phatm2_mndclv",null,null,3,0,77,0,0,0,0,0,null],
["p1","~12","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~13","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~14","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~15","token","token_10055_phatm2_mndclv",null,null,3,1,133,0,0,0,0,0,null],
["p1","~16","token","token_10055_phatm2_mndclv",null,null,3,1,134,0,0,0,0,0,null],
["p1","~15","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~16","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~17","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~18","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null]
["p1","~18","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~19","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~20","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~21","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null],
["p1","~22","token","token_10055_phatm2_mndclv",null,null,3,0,117,0,0,0,0,0,null],
["p1","~23","token","token_10055_phatm2_mndclv",null,null,0,0,118,0,0,5,0,0,null],
["p1","~24","token","token_10055_phatm2_mndclv",null,null,3,0,0,0,0,0,0,0,null]
],
"enemies": {"enemy_1116_liprr_2":[27,9,18,3738,280676],"enemy_1119_vofsd":[48,15,29,15202,794636],"enemy_1121_lifbos_2":[30,6,24,6534,778428]},
"hooks": [1,136,6900,1074,1074,1383,3796,1745,42,121,76,73,0,17719,311,60,105,71,1,42,0,251,0],
"events": {"atk":1074,"deploy":31,"die":50,"dmg":3796,"engage":10,"fx":486,"leak":71,"skill":149,"spawn":127,"status":752},
"fx": {"burst":251,"liberate":3,"palsy":222,"summon":10},
"snaps": "ff349734 43b3c2d1 5e09fdbf 588f8bb7 62eaf80f bf97ee30 1b8c2f7d 647f33c5 e595a119 78a82ab2 0afc7940 29efd966 5faf9bc5 4a9efa2e a6f21a05 1e94de9b a6059141 152adafe 8779d264 2b7ac375 c3fb645c 3dd7bb76 e51f73ed"
"enemies": {"enemy_1116_liprr_2":[27,12,15,5313,373818],"enemy_1119_vofsd":[48,18,29,10828,928079],"enemy_1121_lifbos_2":[30,5,21,7373,1047604]},
"hooks": [1,132,6900,1093,1093,1525,4645,0,47,118,78,76,0,22247,391,86,105,65,1,47,0,328,0],
"events": {"atk":1135,"deploy":27,"die":53,"dmg":4645,"engage":10,"fx":690,"leak":65,"skill":154,"spawn":126,"status":938},
"fx": {"burst":328,"liberate":1,"palsy":352,"summon":9},
"snaps": "92d74d6f 19e080ba 84556549 3ba89a27 8cbe8636 180eb4a2 25fc0509 84e1a783 475aa2d4 5f2d2bc0 9ebcb5d4 67c57af0 d9bb9d96 b315452d 89fc63d4 850bee95 54557caa f7eea76c ba318d70 078c5aeb 16f3e6b9 32dd293c 77966ae0"
},
"diy-128": {
"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",
"about": "5_diy1_b=ironmn/S1/002_ironmn 5_diy2_b=ironmn/S2/002_ironmn 5_diy1_b=ironmn/S3/002_ironmn 5_diy2_b=ironmn/S1/003_ironmn 5_diy1_b=ironmn/S2/003_ironmn 5_diy2_b=ironmn/S3/003_ironmn 6_diy1_a=ironmn/S1 6_diy2_a=ironmn/S2 6_diy1_a=ironmn/S3 6_diy2_b=ironmn/S1 6_diy1_b=ironmn/S2 6_diy2_b=ironmn/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_hard",
"round": 12,
"round": 13,
"seed": 1370290236,
"waveId": "act1autochess_h05",
"time": 192,
"ticks": 5760,
"waveId": "act1autochess_h06",
"time": 211.4,
"ticks": 6342,
"reason": "cleared",
"killed": 7,
"total": 93,
"killed": 0,
"total": 165,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 18,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":7,"total":93,"perfect":false,"leaks":86,"leaksUncounted":0,"coins":0,"dmg":234674,"bossDmg":0,"heal":0,"deaths":16,"layerGains":{}}},
"players": {"p1":{"killed":0,"total":165,"perfect":false,"leaks":165,"leaksUncounted":0,"coins":0,"dmg":28009,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,84,1,14487,5208,0,52,1],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,76,1,13007,6742,0,41,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,114,1,14653,4587,0,18,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,110,1,73028,5712,0,27,2],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,117,1,8944,5276,0,13,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,73,2,51862,6822,0,33,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,0,14757,0,0,56,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,7,0,16360,0,0,56,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,9,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,11,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,13,0,13331,0,0,57,2],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,70,0,14245,2262,0,38,2],
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_ironmn",0,0,182,0,656,7479,0,6,0],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_ironmn",0,0,190,0,6421,7479,0,15,0],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_ironmn",0,0,172,0,1781,7479,0,5,0],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_ironmn",0,0,156,0,8227,5506,0,17,1],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_ironmn",0,0,96,0,1392,5506,0,7,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_ironmn",0,0,91,0,112,5506,0,4,0],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,85,0,1993,4084,0,5,0],
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,3,0,768,2042,0,4,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,5,0,2460,2042,0,5,0],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,7,0,1036,2493,0,5,0],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,9,0,1554,2493,0,7,0],
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,100,0,1610,4986,0,9,0],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_10065_ftzlc":[63,7,56,12751,169866],"enemy_10067_ftsjc":[30,0,30,23858,64809]},
"hooks": [1,117,5760,641,641,734,750,0,23,109,16,16,0,20287,95,37,93,86,1,26,0,0,8],
"events": {"atk":641,"deploy":22,"die":23,"dmg":750,"engage":10,"fx":83,"leak":86,"skill":32,"spawn":107,"status":196},
"fx": {"dodge":8,"dp":5,"explode":14,"ignite":32,"slash":24},
"snaps": "b735394e 44df6814 4ede3523 ceaca815 0990904d 0cf45d51 3f5f612a 3392f0f3 a2cb1d61 6c52af78 9415f3b8 49b70461 7f144b13 c9769b73 a22d4adb 6b2c276d 4b214962 73629044 b830e5dc 0cb0f98c"
"enemies": {"enemy_1249_lysdb_2":[60,0,60,20749,8688],"enemy_1251_lysyta":[51,0,51,9898,18927],"enemy_1254_lypa_2":[54,0,54,26448,394]},
"hooks": [1,190,6342,214,214,214,206,0,23,188,1,1,0,8622,0,43,165,165,1,23,0,0,0],
"events": {"atk":214,"deploy":23,"die":23,"dmg":206,"engage":12,"fx":8,"leak":165,"skill":2,"spawn":179},
"fx": {"shieldBreak":8},
"snaps": "bbce30bd 6cc5009f 649818bd 28c272d5 25acc4e8 ccdb3ca6 e85b1bb8 553fc390 9b2abe11 596023e6 2a2add24 afade29d 3e8049c5 72a07209 61a79135 572cd571 10e94bea 9de78af0 50db5948 6a92b788 812a7698 1fbb52b7"
},
"diy-129": {
"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",
"about": "6_diy1_b=ironmn/S1/002_ironmn 6_diy2_b=ironmn/S2/002_ironmn 6_diy1_b=ironmn/S3/002_ironmn 6_diy2_b=ironmn/S1/003_ironmn 6_diy1_b=ironmn/S2/003_ironmn 6_diy2_b=ironmn/S3/003_ironmn 5_diy1_a=nasti/S1 5_diy2_a=nasti/S2 5_diy1_a=nasti/S3 5_diy2_b=nasti/S1 5_diy1_b=nasti/S2 5_diy2_b=nasti/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_single_normal",
"round": 12,
"round": 13,
"seed": 208547970,
"waveId": "act1autochess_h05",
"time": 213.77,
"ticks": 6413,
"waveId": "act1autochess_h06",
"time": 218.6,
"ticks": 6558,
"reason": "cleared",
"killed": 40,
"total": 159,
"killed": 2,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 260,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":40,"total":159,"perfect":false,"leaks":119,"leaksUncounted":0,"coins":0,"dmg":227420,"bossDmg":0,"heal":4309,"deaths":28,"layerGains":{}}},
"players": {"p1":{"killed":2,"total":102,"perfect":false,"leaks":100,"leaksUncounted":0,"coins":0,"dmg":76712,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,6,9,34156,1865,0,26,1],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,9,7,75183,2374,0,31,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,190,0,7213,9373,0,21,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",95.94,1,188,1,18424,10040,0,33,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,166,13,44376,2544,0,48,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",758.45,1,189,7,35386,2416,0,37,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,4,0,0,1372,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,114,3,11915,4113,0,5,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,8,0,0,2434,2643,7,1],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,103,0,0,5171,1666,3,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,1,0,0,1160,0,0,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,102,0,768,2860,0,2,1]
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_ironmn",0,0,125,0,8151,7479,0,15,1],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_ironmn",2493,1,126,0,5531,4986,0,11,1],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_ironmn",0,0,120,0,5947,7479,0,14,1],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_ironmn",0,0,104,0,6964,5786,0,12,1],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_ironmn",0,0,74,0,3849,5786,0,11,0],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_ironmn",0,0,113,0,4108,5786,0,10,1],
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,66,0,1363,3876,0,7,0],
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,1,0,618,1938,0,2,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,2,0,8489,1938,0,11,1],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,6,2,22195,2367,0,15,1],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,70,0,5327,4734,0,13,1],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,62,0,4169,4734,0,11,0]
],
"enemies": {"enemy_1009_lurker":[72,18,54,2183,64312],"enemy_1019_jshoot_2":[51,13,38,16138,60642],"enemy_1404_msnip":[36,9,27,27401,102466]},
"hooks": [1,190,6413,485,485,717,754,15,68,187,8,22,0,8825,82,56,159,119,1,68,0,0,1],
"events": {"atk":560,"deploy":31,"die":68,"dmg":751,"engage":8,"fx":9,"heal":13,"leak":119,"skill":48,"spawn":171,"status":77},
"fx": {"aoe":3,"displace":3,"dodge":1,"spGain":2},
"snaps": "cb7c5d0f 22a665b0 b0241d3f 8234db69 6d6af92a 4df4630a fbb444f9 7e8e6ab5 d60faeb1 c543720a 31f9bc0d 2619ab64 9dc146d2 0df3dd1d 845dbd2b 7141ab9c aaad4844 c5a03dc3 284e9904 bb59254b 5385151c 09b91371"
"enemies": {"enemy_1267_nhpbr_2":[27,2,25,2555,40000],"enemy_1270_nhstlk_2":[36,0,36,29550,30114],"enemy_9006_actoxi":[39,0,39,24784,6598]},
"hooks": [1,126,6558,330,330,408,408,0,25,125,9,9,0,10014,0,48,102,100,1,25,0,0,0],
"events": {"atk":345,"deploy":24,"die":25,"dmg":399,"engage":12,"fx":18,"leak":100,"skill":18,"spawn":114,"status":40},
"fx": {"shield":16,"zone":2},
"snaps": "8b599cc6 48d99fa5 a69351f0 c11b367c d50588af 8d228d77 ab0fed19 04ac0d7e 376d499d 45cf9b15 9ddaadfb df5d1765 5d30798c 9635f47f 4a5e4307 23278ddd 8d0e5f5e 899ab4fc 4c923b53 4e5ecf46 808c96ee 19aacf11"
},
"diy-130": {
"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",
"about": "5_diy1_b=nasti/S1/002_nasti 5_diy2_b=nasti/S2/002_nasti 5_diy1_b=nasti/S3/002_nasti 6_diy1_a=nasti/S1 6_diy2_a=nasti/S2 6_diy1_a=nasti/S3 6_diy2_b=nasti/S1 6_diy1_b=nasti/S2 6_diy2_b=nasti/S3 6_diy1_b=nasti/S1/002_nasti 6_diy2_b=nasti/S2/002_nasti 6_diy1_b=nasti/S3/002_nasti",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
@@ -5588,73 +5594,355 @@
"round": 13,
"seed": 2862651535,
"waveId": "act1autochess_h06",
"time": 220.7,
"ticks": 6621,
"time": 201.4,
"ticks": 6042,
"reason": "cleared",
"killed": 1,
"killed": 0,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 369,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":1,"total":102,"perfect":false,"leaks":101,"leaksUncounted":0,"coins":0,"dmg":937924,"bossDmg":0,"heal":87780,"deaths":16,"layerGains":{}}},
"players": {"p1":{"killed":0,"total":102,"perfect":false,"leaks":102,"leaksUncounted":0,"coins":0,"dmg":51305,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,118,0,443,7812,0,7,0],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,121,0,1812,10293,0,15,0],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,114,0,13093,5419,0,11,1],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,113,0,37999,8915,0,11,2],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,108,0,91547,38701,0,80,4],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,107,0,159873,57383,0,81,4],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,0,16194,0,0,45,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,9,1,125710,0,0,156,2],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,13,0,31368,0,0,37,2],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,0,459886,0,0,85,3],
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,40530,44,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,10,0,0,0,47250,44,2],
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_nasti",0,0,122,0,4011,7335,0,8,1],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_nasti",2445,1,127,0,4150,4890,0,10,1],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_nasti",0,0,106,0,10347,4890,0,11,1],
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,115,0,1245,3876,0,5,0],
["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,69,0,1536,3876,0,4,0],
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,67,0,1745,3876,0,5,0],
["p1",7,"op","chess_char_6_diy2_b",0,"none",0,0,61,0,2153,4734,0,7,0],
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,68,0,1295,4734,0,5,0],
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,63,0,2411,4734,0,9,0],
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_nasti",0,0,9,0,838,2512,0,2,0],
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_nasti",0,0,10,0,419,2512,0,1,1],
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_nasti",2512,1,6,0,21155,0,0,31,4],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1267_nhpbr_2":[27,0,27,2985,177195],"enemy_1270_nhstlk_2":[36,1,35,21217,245338],"enemy_9006_actoxi":[39,0,39,104320,515392]},
"hooks": [1,126,6621,804,804,1335,1327,183,17,118,24,23,0,28925,0,35,102,101,1,22,0,0,8],
"events": {"atk":921,"deploy":22,"die":17,"dmg":1327,"engage":10,"fx":66,"heal":181,"leak":101,"skill":47,"spawn":116,"status":36},
"fx": {"aoe":14,"dodge":8,"dp":3,"slash":24,"spGain":17},
"snaps": "991457ea e05dc464 b3371c0c 96324561 4e55a051 849b28ac 6aaff9a0 e19921e6 e0540cc5 9dda1ab1 0d299067 9b6f5ee2 e612af65 30447a83 843f3646 1d4d8894 581172e5 afeaf942 e3a403f5 27743746 e033f10e a3b5099b"
"enemies": {"enemy_1267_nhpbr_2":[27,0,27,7657,15836],"enemy_1270_nhstlk_2":[36,0,36,17202,20585],"enemy_9006_actoxi":[39,0,39,23109,14883]},
"hooks": [1,127,6042,193,193,213,213,0,21,123,8,8,0,9667,0,38,102,102,1,21,0,0,0],
"events": {"atk":194,"deploy":23,"die":21,"dmg":207,"engage":12,"fx":16,"leak":102,"skill":16,"spawn":116,"status":28},
"fx": {"shield":16},
"snaps": "4632ae20 24966367 b6d1a168 84949257 4be9835a f237b271 0ac0c2c7 879ea264 9003e9c4 69e2b7ca be0fb246 101b09e5 c3302c1c 8eb425c8 1a169de9 0f5f3e47 a841b3ab 3c71daa4 6448a7de 271dd58e 5e0e8283"
},
"diy-131": {
"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",
"about": "5_diy1_a=necras/S1 5_diy2_a=necras/S2 5_diy1_a=necras/S3 5_diy2_b=necras/S1 5_diy1_b=necras/S2 5_diy2_b=necras/S3 5_diy1_b=necras/S1/002_necras 5_diy2_b=necras/S2/002_necras 5_diy1_b=necras/S3/002_necras 6_diy1_a=necras/S1 6_diy2_a=necras/S2 6_diy1_a=necras/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m03",
"modeId": "mode_multi_funny",
"round": 13,
"round": 12,
"seed": 1967364563,
"waveId": "act1autochess_h06",
"time": 213.2,
"ticks": 6396,
"waveId": "act1autochess_h05",
"time": 189.9,
"ticks": 5697,
"reason": "cleared",
"killed": 34,
"killed": 135,
"total": 135,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 30,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":135,"total":135,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":979200,"bossDmg":0,"heal":40886,"deaths":1,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,1443,1,8,2,24458,0,0,70,7],
["p1",2,"op","chess_char_5_diy2_a",1,null,1443,1,11,10,88149,0,0,75,6],
["p1",3,"op","chess_char_5_diy1_a",2,null,1443,1,13,19,166938,0,0,109,7],
["p1",4,"op","chess_char_5_diy2_b",0,"none",1132,1,15,42,223798,630,0,116,11],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1762,1,4,6,59014,0,0,24,5],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1762,1,9,13,124486,0,0,91,8],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_necras",1882,1,5,1,182,0,0,1,1],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_necras",1882,1,7,7,39594,0,0,40,4],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_necras",1882,1,10,9,63392,0,0,68,7],
["p1",10,"op","chess_char_6_diy1_a",0,null,673,1,12,2,30558,770,0,79,7],
["p1",11,"op","chess_char_6_diy2_a",1,null,123,1,165,5,34816,2763,0,36,3],
["p1",12,"op","chess_char_6_diy1_a",2,null,1443,1,6,1,32142,0,0,29,7],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null],
["p1","~1","token","token_10043_necras_skeltn",null,null,3770,1,19,6,36390,835,0,16,5],
["p1","~2","token","token_10043_necras_skeltn",null,null,3277.2,0,20,0,884,222,22,2,null],
["p1","~3","token","token_10043_necras_skeltn",null,null,3488,0,27,0,4734,4453,4453,9,null],
["p1","~4","token","token_10043_necras_skeltn",null,null,3070,1,33,1,1295,0,0,4,null],
["p1","~5","token","token_10043_necras_skeltn",null,null,13603.2,1,34,0,0,0,0,0,3],
["p1","~6","token","token_10043_necras_skeltn",null,null,3488,0,43,0,0,0,0,0,0],
["p1","~7","token","token_10043_necras_skeltn",null,null,3488,0,44,1,5113,4935,4935,10,null],
["p1","~8","token","token_10043_necras_skeltn",null,null,1744,0,53,0,0,0,0,0,0],
["p1","~9","token","token_10043_necras_skeltn",null,null,3488,0,55,0,0,708,1311,0,0],
["p1","~10","token","token_10043_necras_skeltn",null,null,3488,1,58,0,0,0,0,0,null],
["p1","~11","token","token_10043_necras_skeltn",null,null,4605,1,59,0,0,0,0,0,0],
["p1","~12","token","token_10043_necras_skeltn",null,null,3488,1,60,0,0,316,316,0,null],
["p1","~13","token","token_10043_necras_skeltn",null,null,3191.8,0,63,1,4705,2884,2588,9,null],
["p1","~14","token","token_10043_necras_skeltn",null,null,3488,0,73,0,0,0,0,0,0],
["p1","~15","token","token_10043_necras_skeltn",null,null,3488,0,77,0,0,0,0,0,0],
["p1","~16","token","token_10043_necras_skeltn",null,null,3437.2,0,79,2,5535,4061,4010,10,null],
["p1","~17","token","token_10043_necras_skeltn",null,null,2226.4,0,96,2,5397,6106,4855,10,null],
["p1","~18","token","token_10043_necras_skeltn",null,null,3488,0,107,0,0,211,211,0,0],
["p1","~19","token","token_10043_necras_skeltn",null,null,3386.4,0,112,1,4975,2946,2844,10,null],
["p1","~20","token","token_10043_necras_skeltn",null,null,1744,0,116,0,0,0,0,0,0],
["p1","~21","token","token_10043_necras_skeltn",null,null,3488,0,117,0,0,1228,1429,0,0],
["p1","~22","token","token_10043_necras_skeltn",null,null,3488,0,125,1,5399,3303,3303,10,null],
["p1","~23","token","token_10043_necras_skeltn",null,null,3488,0,140,0,0,105,105,0,0],
["p1","~24","token","token_10043_necras_skeltn",null,null,799.4,0,143,0,4942,7705,5017,9,null],
["p1","~25","token","token_10043_necras_skeltn",null,null,1342,0,159,0,0,402,0,0,0],
["p1","~26","token","token_10043_necras_skeltn",null,null,3462.6,0,160,1,5453,2455,2430,10,null],
["p1","~27","token","token_10043_necras_skeltn",null,null,3488,0,166,0,0,1719,1920,0,0],
["p1","~28","token","token_10043_necras_skeltn",null,null,3488,0,173,2,5504,1137,1137,10,null],
["p1","~29","token","token_10043_necras_skeltn",null,null,3488,1,178,0,486,0,0,1,0],
["p1","~30","token","token_10043_necras_skeltn",null,null,3059,1,181,0,862,11,0,2,null]
],
"enemies": {"enemy_2021_syfish":[105,105,0,7326,403200],"enemy_2025_syufo":[30,30,0,42579,576000]},
"hooks": [1,181,5697,1208,1208,1763,1943,268,136,158,81,80,0,54150,41,88,135,0,1,136,0,15,0],
"events": {"atk":1208,"deploy":43,"die":158,"dmg":1943,"engage":27,"fx":504,"heal":69,"skill":161,"spawn":180,"status":119},
"fx": {"aoe":150,"burst":15,"grow":268,"phase":18,"sleep":23,"summon":30},
"snaps": "e06bb91d 3d9603df 235a870d 610af988 e5adbf49 c087e51c 9580ba01 de2151cd 8ec08976 09a31624 b5a17549 01ba521e afdab2ca 7c0620a6 e7ef14a1 498df6e0 62fd682d cc89ff94 12c6a7a5"
},
"diy-132": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=necras/S1 6_diy2_b=necras/S2 6_diy1_b=necras/S3 6_diy2_b=necras/S1/002_necras 6_diy1_b=necras/S2/002_necras 6_diy2_b=necras/S3/002_necras 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_single_hard",
"round": 13,
"seed": 326579774,
"waveId": "act1autochess_h06",
"time": 198.8,
"ticks": 5964,
"reason": "cleared",
"killed": 123,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 287,
"rngDraws": 26,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":34,"total":123,"perfect":false,"leaks":89,"leaksUncounted":0,"coins":0,"dmg":303954,"bossDmg":0,"heal":0,"deaths":18,"layerGains":{}}},
"players": {"p1":{"killed":123,"total":123,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1100899,"bossDmg":0,"heal":80557,"deaths":2,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,115,2,12461,3730,0,44,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,94,11,80882,4988,0,58,2],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,148,5,20771,8078,0,95,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",273.68,1,147,5,28658,11598,0,87,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,6,7,64863,0,0,146,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,7,4,96319,0,0,156,3],
["p1",1,"op","chess_char_5_diy1_a",2,null,213.74,1,7,0,380,1914,0,4,1],
["p1",2,"op","chess_char_5_diy2_b",2,"none",2908,1,8,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,6,0,0,0,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",3334,1,9,0,0,0,0,0,0],
["p1",5,"op","chess_char_6_diy1_b",0,"none",1762,1,3,1,13338,0,0,6,2],
["p1",6,"op","chess_char_6_diy2_b",1,"none",1762,1,4,1,23910,0,0,13,1],
["p1",7,"op","chess_char_6_diy1_b",2,"none",1762,1,11,6,59742,0,0,38,5],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_002_necras",57.23,1,13,9,71000,1905,0,61,6],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_002_necras",1962,1,5,2,13327,0,0,16,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_necras",1962,1,10,39,323654,0,0,108,8],
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,12,8,47570,0,0,178,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,97,3,30621,3020,0,84,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],
["p1","~1","token","token_10043_necras_skeltn",null,null,2651.32,0,24,2,3785,4349,4008,10,null],
["p1","~2","token","token_10043_necras_skeltn",null,null,1744,0,29,0,566,0,0,2,null],
["p1","~3","token","token_10043_necras_skeltn",null,null,9417.6,1,30,1,3565,1716,2080,3,4],
["p1","~4","token","token_10043_necras_skeltn",null,null,0,0,31,0,4439,5232,0,6,0],
["p1","~5","token","token_10043_necras_skeltn",null,null,3488,0,32,0,0,0,0,0,null],
["p1","~6","token","token_10043_necras_skeltn",null,null,3488,1,39,0,6254,8534,8534,12,null],
["p1","~7","token","token_10043_necras_skeltn",null,null,3459.5,0,40,0,3191,1182,2022,9,0],
["p1","~8","token","token_10043_necras_skeltn",null,null,3488,1,41,0,9098,57,57,19,null],
["p1","~9","token","token_10043_necras_skeltn",null,null,13603.2,1,45,43,416415,11542,14835,44,7],
["p1","~10","token","token_10043_necras_skeltn",null,null,0,0,46,0,3745,3488,0,7,null],
["p1","~11","token","token_10043_necras_skeltn",null,null,3488,1,49,1,554,0,0,3,null],
["p1","~12","token","token_10043_necras_skeltn",null,null,3488,1,50,0,0,0,0,0,null],
["p1","~13","token","token_10043_necras_skeltn",null,null,3459.5,0,52,0,231,28,0,1,0],
["p1","~14","token","token_10043_necras_skeltn",null,null,0,0,53,0,231,1744,0,1,0],
["p1","~15","token","token_10043_necras_skeltn",null,null,1417.62,0,54,1,6384,5341,3270,13,0],
["p1","~16","token","token_10043_necras_skeltn",null,null,2683.81,0,58,0,5063,804,0,10,0],
["p1","~17","token","token_10043_necras_skeltn",null,null,1371.71,0,67,0,173,372,0,1,0],
["p1","~18","token","token_10043_necras_skeltn",null,null,2423.35,0,69,1,6119,10888,9824,12,0],
["p1","~19","token","token_10043_necras_skeltn",null,null,1744,0,93,0,243,0,0,1,null],
["p1","~20","token","token_10043_necras_skeltn",null,null,0,0,94,0,243,1744,0,1,null],
["p1","~21","token","token_10043_necras_skeltn",null,null,0,0,95,0,694,1744,0,3,null],
["p1","~22","token","token_10043_necras_skeltn",null,null,3488,0,96,0,231,0,0,1,0],
["p1","~23","token","token_10043_necras_skeltn",null,null,3459.5,0,98,1,6252,8879,8851,14,0],
["p1","~24","token","token_10043_necras_skeltn",null,null,1451.54,0,100,0,5318,5860,3824,10,0],
["p1","~25","token","token_10043_necras_skeltn",null,null,1247.61,0,105,0,462,496,0,2,0],
["p1","~26","token","token_10043_necras_skeltn",null,null,3431.01,0,112,0,5306,6183,7477,12,0],
["p1","~27","token","token_10043_necras_skeltn",null,null,1083.02,0,113,0,3029,2729,619,9,0],
["p1","~28","token","token_10043_necras_skeltn",null,null,1451.54,0,130,0,3387,2407,541,7,null],
["p1","~29","token","token_10043_necras_skeltn",null,null,0,0,136,2,4024,6576,4020,10,0],
["p1","~30","token","token_10043_necras_skeltn",null,null,27.29,0,139,0,6115,11686,8692,12,0],
["p1","~31","token","token_10043_necras_skeltn",null,null,0,0,147,0,3006,3488,0,6,null],
["p1","~32","token","token_10043_necras_skeltn",null,null,1495.81,0,154,0,231,248,0,1,0],
["p1","~33","token","token_10043_necras_skeltn",null,null,1744,1,155,0,0,0,0,0,0],
["p1","~34","token","token_10043_necras_skeltn",null,null,0,0,156,0,647,1744,0,2,0],
["p1","~35","token","token_10043_necras_skeltn",null,null,0,0,160,0,2774,3716,352,6,0],
["p1","~36","token","token_10043_necras_skeltn",null,null,1619.9,0,165,0,231,124,0,1,0],
["p1","~37","token","token_10043_necras_skeltn",null,null,1619.9,0,167,0,231,124,0,1,0],
["p1","~38","token","token_10043_necras_skeltn",null,null,3431.01,1,168,2,4656,749,940,11,0],
["p1","~39","token","token_10043_necras_skeltn",null,null,3459.5,1,170,0,462,391,610,1,0]
],
"enemies": {"enemy_1019_jshoot_2":[57,57,0,13712,255302],"enemy_1299_ymkilr_2":[27,27,0,103761,492687],"enemy_1422_lrsldr_2":[39,39,0,3532,355830]},
"hooks": [1,177,5964,1154,1154,1519,1519,139,134,155,39,39,0,31791,2,137,123,0,1,134,0,0,0],
"events": {"atk":1154,"deploy":52,"die":155,"dmg":1519,"engage":45,"fx":323,"heal":88,"skill":78,"spawn":176,"status":3},
"fx": {"aoe":143,"grow":139,"sleep":2,"summon":39},
"snaps": "fca07158 7e491823 1379df7d 43eca66a 1670212d 34467249 ce2fd1dd 106168b3 2159db6f 66183b01 3f2c0eb3 117ea2a7 1cd2cb58 bec7ece2 6863bf40 ef5528a5 10c16399 2b2413f1 c9fee135 a483f71b"
},
"diy-133": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_multi_normal",
"round": 12,
"seed": 953190138,
"waveId": "act1autochess_h05",
"time": 218.27,
"ticks": 6548,
"reason": "cleared",
"killed": 173,
"total": 243,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 117,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":173,"total":243,"perfect":false,"leaks":70,"leaksUncounted":0,"coins":0,"dmg":509327,"bossDmg":0,"heal":35627,"deaths":7,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,198,1,9062,5208,0,41,1],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",3121.56,1,252,5,24943,3620,0,81,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1529,1,261,6,18839,7756,0,40,2],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1904,1,151,100,195205,11634,0,107,6],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,15,14,93664,21198,0,140,3],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",3045.96,1,233,10,25732,3776,0,42,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,8,15364,0,0,83,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,5,16,47409,0,0,148,4],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,11,0,0,0,19362,59,3],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,13,0,0,0,16264,58,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,6,12,18431,0,0,107,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,14,1,60680,0,0,133,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_1006_shield_3":[21,0,21,13429,72237],"enemy_1043_zomsbr":[66,27,39,93,102526],"enemy_1402_tgshd_2":[36,7,29,14872,129191]},
"hooks": [1,148,6396,693,693,920,1011,11182,52,141,8,21,0,10798,12327,37,123,89,1,52,0,0,0],
"events": {"atk":764,"deploy":22,"die":52,"dmg":945,"engage":6,"fx":503,"leak":89,"skill":47,"spawn":132,"status":927},
"fx": {"displace":19,"dodge":5,"expose":479},
"snaps": "d456be72 24986da0 263291b2 6601c930 43a57b58 57e6c19a e9793217 f2307500 9bf48dd7 a1e1185f f757a9dd 9feca978 f1099997 885b8762 df7edeeb d75a4c78 dbb2b23e 3942d750 75b12534 b102163d 3fee0d49 966739a7"
"enemies": {
"enemy_1195_sfyin": [45,26,19,13482,144786],
"enemy_1196_msfyin": [52,45,7,0,91],
"enemy_1197_sfshu": [39,28,11,13145,181727],
"enemy_1198_msfshu": [84,61,23,0,195],
"enemy_1199_sfjin": [12,11,1,26565,182424],
"enemy_1200_msfjin": [11,2,9,0,104]
},
"hooks": [1,264,6548,1492,1492,1952,1915,117,180,250,34,33,0,39609,222,69,243,70,1,180,0,0,37],
"events": {"atk":1492,"deploy":18,"die":180,"dmg":1915,"engage":10,"fx":323,"heal":117,"leak":70,"skill":67,"spawn":258,"status":456},
"fx": {"aoe":52,"dodge":37,"dp":114,"slash":55,"summon":65},
"snaps": "bc6da2c9 ceebca03 cdedd66c 4125c5f9 0142c8a8 b8d589a4 0abad4db 48f74225 831cf5aa c20fb442 0f7f866d 1b522268 513dbd00 272f78e4 f8c39620 6413c2bd 7ee6e8b4 83bfc984 c53e60c8 bee2518e 35f20f3b 28db8fee"
},
"diy-134": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_multi_hard",
"round": 12,
"seed": 3608019556,
"waveId": "act1autochess_h05",
"time": 208.03,
"ticks": 6241,
"reason": "cleared",
"killed": 2,
"total": 60,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1049,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":2,"total":60,"perfect":false,"leaks":58,"leaksUncounted":0,"coins":0,"dmg":558618,"bossDmg":0,"heal":29304,"deaths":21,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,61,0,13900,3730,0,16,1],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,68,1,55591,4748,0,37,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,87,0,942,5770,0,24,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,85,0,4428,7240,0,24,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,5,0,14792,0,0,181,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,8,0,30614,0,0,177,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,6,1,137220,0,0,51,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,9,0,261767,0,0,77,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,88,0,0,3201,3296,8,1],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,11,0,0,15092,26008,21,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,71,0,21082,17194,0,40,1],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,89,0,18281,2860,0,22,1]
],
"enemies": {"enemy_1045_hammer_2":[12,0,12,4838,121840],"enemy_1061_zomshd":[48,2,46,54996,436778]},
"hooks": [1,89,6241,759,759,1939,1993,26777,23,81,15,25,0,22089,2415,24,60,58,1,23,0,0,0],
"events": {"atk":1095,"deploy":29,"die":23,"dmg":1979,"engage":10,"fx":13,"heal":53,"leak":58,"skill":54,"spawn":72,"status":266},
"fx": {"aoe":8,"dodge":4,"spGain":1},
"snaps": "eb37fe41 2feae3f0 5c31d8ff f72ff3aa 32f606ba 526ce9c0 49700653 5006595f c5c6846e 6d2b9b8e 4b024976 e92d04be 471cd084 7f606005 f119fdc0 5677942e 4fe9bcbf 9d0af4bf 4077bae2 9168ce11 ab027285"
},
"diy-135": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_single_normal",
"round": 13,
"seed": 3994643046,
"waveId": "act1autochess_h06",
"time": 216.23,
"ticks": 6487,
"reason": "cleared",
"killed": 139,
"total": 150,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 391,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":139,"total":150,"perfect":false,"leaks":11,"leaksUncounted":0,"coins":0,"dmg":1550587,"bossDmg":0,"heal":109719,"deaths":4,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,2604,1,167,0,3442,2604,0,30,1],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",2646.22,1,7,1,22576,4540,0,130,2],
["p1",3,"op","chess_char_6_diy1_a",2,null,1529,1,169,26,218591,23279,0,102,10],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1494.95,1,11,63,603310,32568,0,159,15],
["p1",5,"op","chess_char_6_diy1_a",2,null,1912.7,1,152,2,28838,15194,0,91,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",2661.84,1,15,10,107218,19493,0,155,5],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,0,9284,0,0,49,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,12,2,55255,0,0,111,2],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,14,0,30553,0,0,40,2],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,35,471521,0,0,80,3],
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,52763,71,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,168,0,0,23065,56956,50,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1249_lysdb_2":[60,57,3,19813,650353],"enemy_1251_lysyta":[51,49,2,2808,252854],"enemy_1329_cbshld":[39,33,6,98122,647381]},
"hooks": [1,169,6487,1589,1589,4054,3954,233,143,154,49,47,0,47498,0,146,150,11,1,143,0,0,40],
"events": {"atk":1708,"deploy":16,"die":143,"dmg":3954,"engage":10,"fx":494,"heal":232,"leak":11,"skill":96,"spawn":165,"status":50},
"fx": {"aoe":79,"dodge":40,"dp":114,"shieldBreak":60,"slash":197,"spGain":4},
"snaps": "3a143536 45205b96 e8d7d1d8 00f7e32c ce3c78b4 2407bf14 13b01dbe 3fec0a84 ee21dba1 0cc379c6 b04c03a9 9eee7fbb b11dc910 c1766ce2 0f77c84f 9fe87e87 75e19b69 4a45b886 895b892d 6b459f6c cfac3759 f5653f35"
},
"diy-136": {
"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_m04",
"modeId": "mode_multi_abyss",
"round": 13,
"seed": 2354181867,
"waveId": "act1autochess_h06",
"time": 225.6,
"ticks": 6768,
"reason": "cleared",
"killed": 1,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 130,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":1,"total":123,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":293038,"bossDmg":0,"heal":0,"deaths":24,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,149,0,281,5595,0,10,0],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,89,1,41351,4988,0,15,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,153,0,27405,12694,0,18,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,152,0,64271,11872,0,52,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,4,0,59475,0,0,141,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,5,0,100254,0,0,162,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_1006_shield_3":[21,0,21,9388,58708],"enemy_1043_zomsbr":[66,1,65,8698,238677],"enemy_1402_tgshd_2":[36,0,36,17063,131985]},
"hooks": [1,153,6768,459,459,2469,2495,24678,25,147,7,24,0,10141,11244,28,123,122,1,25,0,0,0],
"events": {"atk":470,"deploy":27,"die":25,"dmg":2389,"engage":6,"fx":257,"leak":122,"skill":44,"spawn":132,"status":875},
"fx": {"displace":30,"dodge":6,"expose":221},
"snaps": "3b9227e1 1631291e 032dd42b e8c97fce 06b2c450 146801be a9ba58c8 42440c2a ee76d4c2 8ac7769c f06d9109 be306e36 b9697bbb 7631a029 e3f80b10 e1a55253 af28ac55 b4ecedd5 6565411e 93bc5900 9a306169 33d6050f e195f073"
}
}
}
+72
View File
@@ -0,0 +1,72 @@
// test/sim/feedback5-o23-engine.test.js — the additive engine rule the 0.2.0 自选 kit of 白铁 needs: ally targets
// (Battle.setAllyTarget / allyTargetsInKeys / isAllyTarget, ai.js acquireTargets / performAttack). 白铁's 铁钳号·原型机 is a
// summon of the enemy camp (PRTS 铁钳号·原型机 备注 "该召唤物阵营为敌方") that "可被我方干员攻击但不受伤害": our attacks select a
// registered ally standing on the attacker's range after every enemy (嘲讽等级 −2), ranged attacks fly to it; heals never
// select it; with none registered the attack loop is exactly the old one. Cancelling the damage it takes is the kit's job
// (test/content/op_ironmn.test.js); here a plain guard is registered and takes the hits.
// Run: node --test test/sim/feedback5-o23-engine.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
import { acquireTargets } from '../../server/sim/ai.js';
const ENEMIES = { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e9, speed: 0, mass: 0 }) };
const CHESS = {
test_guard_a: chessRec({ id: 'test_guard_a', profession: 'WARRIOR', stats: { maxHp: 1e7, atk: 300, def: 0, res: 0 } }),
test_sniper_a: chessRec({ id: 'test_sniper_a', profession: 'SNIPER', stats: { maxHp: 2000, atk: 200, def: 0, res: 0 }, rangeGrid: [[0, 0], [0, 1], [0, 2], [0, 3]] }),
test_medic_a: chessRec({ id: 'test_medic_a', profession: 'MEDIC', subProf: 'physician', stats: { maxHp: 2000, atk: 100, def: 0, res: 0 }, rangeGrid: [[0, 0], [0, 1], [0, 2]] }),
};
function field() {
const h = makeBattle({
defs: { enemies: ENEMIES, chess: CHESS }, timeLimit: 600, autoFinish: false, seed: 3, hooks: ['damaged'], captureNoisy: true,
units: [
{ uid: 1, chessId: 'test_guard_a', row: 10, col: 4 }, // melee 1-1: (10, 4), (10, 5)
{ uid: 2, chessId: 'test_guard_a', row: 10, col: 5 }, // the ally target
{ uid: 3, chessId: 'test_sniper_a', row: 10, col: 2 }, // ranged: (10, 2) … (10, 5)
{ uid: 4, chessId: 'test_medic_a', row: 10, col: 3 },
],
});
h.step();
return { h, guard: h.unit(1), target: h.unit(2), sniper: h.unit(3), medic: h.unit(4) };
}
test('ally targets: none registered ⇒ nobody selects an ally; registered ⇒ the attackers whose range holds it select it, after every enemy', () => {
const { h, guard, target, sniper, medic } = field();
assert.equal(h.b.isAllyTarget(target), false);
assert.deepEqual(acquireTargets(h.b, guard, guard.profile), [], 'not registered: no target');
assert.ok(h.b.setAllyTarget(target, true));
assert.ok(h.b.isAllyTarget(target));
assert.deepEqual(h.b.allyTargetsInKeys(guard.rangeKeys, guard).map((u) => u.id), [target.id]);
assert.deepEqual(h.b.allyTargetsInKeys(target.rangeKeys, target), [], 'never itself');
assert.deepEqual(acquireTargets(h.b, guard, guard.profile).map((u) => u.id), [target.id], 'no enemy: the ally target');
assert.deepEqual(acquireTargets(h.b, medic, medic.profile), [], 'a healer never selects it');
// an enemy in range comes first; all-in-range / two targets take both
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
h.step();
assert.deepEqual(acquireTargets(h.b, guard, guard.profile).map((u) => u.id), [e.id], 'one target: the enemy');
assert.deepEqual(acquireTargets(h.b, guard, { ...guard.profile, maxTargets: 2 }).map((u) => u.id), [e.id, target.id], 'two: the enemy, then it');
assert.deepEqual(acquireTargets(h.b, guard, { ...guard.profile, allInRange: true }).map((u) => u.id), [e.id, target.id], 'all in range: both');
h.b.setAllyTarget(target, false);
assert.deepEqual(acquireTargets(h.b, guard, { ...guard.profile, maxTargets: 2 }).map((u) => u.id), [e.id], 'dropped: enemies only');
void sniper;
checkInvariants(h.b);
});
test('ally targets: attacks on it are real attacks (melee instant, ranged by projectile); off the field it is skipped', () => {
const { h, guard, target, sniper } = field();
h.b.setAllyTarget(target, true);
h.run(3);
const by = (src) => h.hooksOf('damaged').filter((c) => c.source === src && c.target === target && c.dmg?.isAttack);
assert.ok(by(guard).length >= 1, 'the guard hits it');
assert.ok(by(sniper).length >= 1, 'the sniper hits it');
let flew = false;
for (let i = 0; i < 90 && !flew; i++) { h.step(); flew = h.b.projectiles.list.some((p) => p.source === sniper && p.target === target); }
assert.ok(flew, 'by projectile');
h.b.retreat(target, { reason: 'retreat' });
h.step();
assert.deepEqual(h.b.allyTargetsInKeys(guard.rangeKeys, guard), [], 'off the field: skipped while registered');
assert.ok(h.b.isAllyTarget(target));
checkInvariants(h.b);
});