feedback5: merge O17 — 自选 kits for 琴柳, 可露希尔 (with her 指挥中心) and 澄闪; golden: only diy moved
This commit is contained in:
commit
000ef7052d
8 files changed
+2195
-99
No files matched your search
@@ -0,0 +1,346 @@
|
||||
// test/content/op_closur.test.js — the 自选 operator kit of 可露希尔 (char_4228_closur, 6★ 战术家; kit
|
||||
// server/sim/content/kits/ops/op-closur.js) and of her 援军 指挥中心 (token_10066_closur_ourbase), fielded the production way (a
|
||||
// DIY slot + its `diy` pick, simdata getDiy; the 指挥中心 as the placed hand piece of her player) in every form: tiers 5 / 6,
|
||||
// normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or TAC-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_closur.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
import { BASE, SHIELD_KEY, S2_KEY, RHODES_KEY, SLOWDOWN } from '../../server/sim/content/kits/ops/op-closur.js';
|
||||
import { COLS } from '../../server/sim/constants.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const CL = 'char_4228_closur';
|
||||
const FORMS = BACKUPS.units[CL].forms;
|
||||
const TOKREC = BACKUPS.tokens[BASE];
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const TX = 'uniequip_002_closur';
|
||||
const S1 = 'skchr_closur_1', S2 = 'skchr_closur_2', S3 = 'skchr_closur_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 talentOf = (tier, elite, mod, i) => modOf(tier, elite ? mod : null)?.talentChanges?.find((t) => t.talentIndex === i) ?? formOf(tier, elite).talents.find((t) => t.index === i);
|
||||
const tokVariant = (tier, elite) => TOKREC.variants[`${CL}@${statusOf(tier, elite)}`];
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_atk: dummy('enemy_atk', { atk: 2000, bat: 1 }) };
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, TX].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
const YAK = 'chess_char_1_02_a', TEXAS = 'chess_char_1_08_a', PODEGO = 'chess_char_1_13_a';
|
||||
const tk = (r, c) => r * COLS + c;
|
||||
|
||||
/**
|
||||
* 可露希尔 as uid 1 at (row, col) facing RIGHT; `base` = the 指挥中心 piece's tile (uid 2) or null; `others` as given.
|
||||
*/
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, base = [10, 6], others = [], seed = 5 } = {}) {
|
||||
const op = { uid: 1, diy: { slot: SLOT[tier], charId: CL, skillIndex: skill, uniEquipId: mod }, elite, row, col };
|
||||
const piece = base ? [{ uid: 2, kind: 'token', tokenId: BASE, ownerUid: 1, row: base[0], col: base[1] }] : [];
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack'], captureNoisy: true,
|
||||
units: [op, ...piece, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1), t: h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === BASE && a.alive) ?? null };
|
||||
}
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
const dpOf = (h) => h.b.players[0].dp;
|
||||
const hitsOn = (h, u, e) => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e);
|
||||
/** Record the DP gains' times. */
|
||||
function dpLog(h) {
|
||||
const log = [];
|
||||
let last = dpOf(h);
|
||||
h.b.on('tick', () => { const v = dpOf(h); if (v !== last) { log.push([h.b.time, v - last]); last = v; } });
|
||||
return log;
|
||||
}
|
||||
|
||||
test('可露希尔 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-3, 战术家 (ranged physical, hits air, blocks 1), ground-targetable, the triggers (S1 at full SP); the placed 指挥中心 deploys with its variant\'s stats, no attack, 禁疗, blocks 2, its range by her skill (x-5 / x-4 / x-6); no generic 援军', () => {
|
||||
assert.equal(OPERATOR_KITS[CL], KITS[CL]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u, t } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, sk = form.skills[skill];
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [CL, SLOT[tier], sk.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], [1, 'ranged', true, 'phys', 'tactician', 1], `${label(f)}: 战术家`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-3`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
assert.deepEqual([u.skill.rule, u.skill.spCost], [skill === 0 ? 'SP_FULL' : sk.trigger.rule, sk.spCost], `${label(f)}: trigger`);
|
||||
if (skill > 0) assert.deepEqual([sk.trigger.rule, sk.trigger.rawRule], ['SP_FULL', 'ALWAYS'], `${label(f)}: the 战术家 row`);
|
||||
else assert.equal(sk.trigger.rule, 'DEFAULT', `${label(f)}: the data's AUTO rule (the kit fires it at full SP)`);
|
||||
const v = tokVariant(tier, elite);
|
||||
assert.ok(t && t.uid === 2 && t.tileR === 10 && t.tileC === 6, `${label(f)}: the placed 指挥中心 on its tile`);
|
||||
assert.deepEqual([t.base.maxHp, t.base.def, t.s.blockCnt, !!t.profile.noAttack, !!t.s.flags.noHeal], [v.stats.maxHp, v.stats.def, 2, true, true], `${label(f)}: 指挥中心`);
|
||||
const grid = skill === 0 ? v.skill.rangeGrid : v.bySkill[String(skill)].skill.rangeGrid;
|
||||
assert.deepEqual(t.liveRangeGrid, grid, `${label(f)}: its range by her S${skill + 1}`);
|
||||
assert.equal(h.b.allyUnits.filter((a) => a.defId === 'token_tactician_reinforce').length, 0, `${label(f)}: no generic 援军`);
|
||||
assert.equal(u.trait.reinforcement, t, `${label(f)}: her 援军`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
assert.deepEqual([modOf(5, TX).attr, modOf(6, TX).attr], [{ maxHp: 150, atk: 20, def: 15 }, { maxHp: 180, atk: 30, def: 25 }]);
|
||||
assert.deepEqual([tokVariant(5, false).stats.maxHp, tokVariant(5, true).stats.maxHp, tokVariant(5, true).skill.rangeId, tokVariant(5, true).bySkill['1'].skill.rangeId, tokVariant(5, true).bySkill['2'].skill.rangeId], [2620, 3054, 'x-5', 'x-4', 'x-6']);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 可露希尔 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(CL));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(CL), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: CL, skillIndex: 1, uniEquipId: TX } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[5]]: { charId: CL, skillIndex: 1, uniEquipId: TX } } });
|
||||
});
|
||||
|
||||
test('精准投放 without a placed piece: the 指挥中心 comes on the tactical point (a walkable tile of her range, the enemy path first); her leaving withdraws it for good, her redeploy brings a new one; knocked out it sits in its 战术点形态 for 15 s (off the fight, its tile kept, no 援军) and comes back at full HP', () => {
|
||||
const { h, u, t } = field({ tier: 6, elite: true, mod: TX, skill: 0, base: null });
|
||||
assert.ok(t && t.uid == null, 'summoned');
|
||||
assert.ok(u.baseRangeKeys.includes(tk(t.tileR, t.tileC)) && h.b.grid.groundPassable(t.tileR, t.tileC), 'a walkable tile of her range');
|
||||
assert.ok(h.b.groundPathTiles().has(tk(t.tileR, t.tileC)), 'on an enemy ground path');
|
||||
done(h);
|
||||
// the placed piece: knock-out ⇒ 战术点形态
|
||||
const r = field({ tier: 5, elite: true, mod: TX, skill: 1, others: [{ uid: 3, chessId: YAK, row: 11, col: 6 }] });
|
||||
const refresh = tokVariant(5, true).talents.find((x) => x.bb.interval != null).bb.interval;
|
||||
assert.equal(refresh, 15);
|
||||
r.h.b.kill(r.t, null);
|
||||
assert.ok(!r.t.alive && !r.t.removed && r.t.mem.closurTac, 'in its 战术点形态');
|
||||
assert.ok(r.h.b.isReservedTile(10, 6), 'its tile kept');
|
||||
r.u.skill.gainSp(999);
|
||||
r.h.run(0.5);
|
||||
assert.equal(r.h.unit(3).findBuff(`${S2_KEY}:${r.u.id}`), null, 'no 援军 while it is down (角峰 in its range unmarked)');
|
||||
r.h.run(refresh - 0.6);
|
||||
assert.ok(!r.t.alive, 'still down before 15 s');
|
||||
r.h.run(0.3);
|
||||
assert.ok(r.t.alive && r.t.tileR === 10 && r.t.tileC === 6, 'back on its tile after 15 s');
|
||||
approx(r.t.hp, r.t.s.maxHp, 'at full HP');
|
||||
r.h.run(0.2);
|
||||
assert.ok(r.h.unit(3).findBuff(`${S2_KEY}:${r.u.id}`), '角峰 marked again (S2 runs)');
|
||||
// her leaving: withdrawn for good; her redeploy: a new one on the board tile
|
||||
r.h.b.kill(r.u, null);
|
||||
assert.ok(!r.t.alive && r.t.removed, 'withdrawn with her (no 战术点形态)');
|
||||
r.h.b.redeploy(r.u, { free: true });
|
||||
const n = r.h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === BASE && a.alive);
|
||||
assert.ok(n && n !== r.t && n.tileR === 10 && n.tileC === 6, 'a new 指挥中心 on the board tile');
|
||||
done(r.h);
|
||||
});
|
||||
|
||||
test('Trait: her damage on an enemy blocked by her 援军 — the 指挥中心 or an ally of its range — ×atk_scale (1.5); ×1 vs an enemy blocked by an ally outside its range', () => {
|
||||
for (const f of [[5, false, null], [6, true, TX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
// S1 carried: the 指挥中心's x-5 around (10,6) holds (11,6) (角峰), not (11,7) (德克萨斯)
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, others: [{ uid: 3, chessId: YAK, row: 11, col: 6 }, { uid: 4, chessId: TEXAS, row: 11, col: 7 }] });
|
||||
const scale = formOf(tier, elite).trait.bb.atk_scale;
|
||||
assert.equal(scale, 1.5);
|
||||
for (const [pos, blocker, mul] of [[[10, 6], t, scale], [[11, 6], h.unit(3), scale], [[11, 7], h.unit(4), 1]]) {
|
||||
const e = h.spawn('enemy_dummy', { pos });
|
||||
assert.ok(h.runUntil(() => e.blockedBy === blocker && hitsOn(h, u, e).length > 0, 5), `${label(f)}: ${pos} blocked and hit`);
|
||||
const c = hitsOn(h, u, e)[0];
|
||||
approx(c.amount, u.s.atk * mul, `${label(f)}: ×${mul} on the enemy blocked at ${pos}`);
|
||||
h.b.kill(e, null);
|
||||
h.step();
|
||||
}
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 递归策略 (AUTO, at full SP): every 援军 — the 指挥中心 and the allies of its x-5, not one outside, not a 孤立 one — gets one 护盾 layer (不叠加); 3 DP, then 4, 5 … up to 6 (normal) / 7 (elite), one at a time every 8 / n s from half that; the count restarts with a redeploy', () => {
|
||||
for (const f of [[5, false, null], [6, true, TX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.duration, sk.bb.cost, sk.bb.cost_per_add, sk.bb.cost_add_max, sk.bb.shield_cnt, sk.initSp], [8, 3, 1, elite ? 7 : 6, 1, 0], label(f));
|
||||
const others = [{ uid: 3, chessId: YAK, row: 11, col: 6 }, { uid: 4, chessId: TEXAS, row: 11, col: 7 }, { uid: 5, chessId: PODEGO, row: 9, col: 6 }];
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, others });
|
||||
h.b.addBuff(h.unit(5), { key: 'test:isolated', flags: { isolated: true } });
|
||||
const log = dpLog(h);
|
||||
h.b.players[0].dp = 0;
|
||||
const casts = [];
|
||||
h.b.on('skillStart', (c) => { if (c.unit === u) casts.push(h.b.time); });
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, sk.spCost + 1), `${label(f)}: cast at full SP, no enemy on the field`);
|
||||
h.step();
|
||||
for (const a of [t, h.unit(3)]) assert.equal(a.findBuff(`${SHIELD_KEY}:${u.id}`)?.shieldHits, 1, `${label(f)}: ${a.defId} one 护盾 layer`);
|
||||
for (const a of [h.unit(4), h.unit(5), u]) assert.equal(a.findBuff(`${SHIELD_KEY}:${u.id}`), null, `${label(f)}: ${a.defId} no 护盾`);
|
||||
// the 护盾 takes one hit whole
|
||||
const e = h.spawn('enemy_atk', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => !t.findBuff(`${SHIELD_KEY}:${u.id}`), 4), `${label(f)}: spent by a hit`);
|
||||
approx(t.hp, t.s.maxHp, `${label(f)}: that hit blocked whole`);
|
||||
h.b.kill(e, null);
|
||||
// the DP of the casts: n = 3, 4, … ≤ max; each over 8 s every 8 / n from half that
|
||||
const want = [];
|
||||
for (let k = 0; k < 6; k++) want.push(Math.min(sk.bb.cost_add_max, sk.bb.cost + k));
|
||||
h.runUntil(() => u.skill.activations >= 6 && !u.skill.active, 6 * (sk.spCost + 9));
|
||||
for (let k = 0; k < 6; k++) {
|
||||
const s0 = casts[k], n = want[k];
|
||||
const gains = log.filter(([tt]) => tt > s0 - 1e-9 && tt < s0 + 8 + 0.05);
|
||||
assert.equal(gains.reduce((a, [, d]) => a + d, 0), n, `${label(f)}: cast ${k + 1} gives ${n}`);
|
||||
approx(gains[0][0] - s0, 8 / n / 2, `${label(f)}: cast ${k + 1} first DP after ${8 / n / 2} s`, 0.06);
|
||||
}
|
||||
// a redeploy restarts the count
|
||||
h.b.retreat(u);
|
||||
h.b.redeploy(u, { free: true });
|
||||
const before = dpOf(h);
|
||||
h.runUntil(() => u.skill.activations >= 7 && !u.skill.active, sk.spCost + 10);
|
||||
assert.equal(dpOf(h) - before, sk.bb.cost, `${label(f)}: back to ${sk.bb.cost} after a redeploy`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 模型扩展: +cost DP at once and 15 one per 2 s; ATK +atk, 2 targets; every 援军 (x-4) DEF +def and block +1; the enemies they block are her targets beyond her range; an operator of hers deployed on the 指挥中心\'s range meanwhile gives back ceil(40 % × its cost) — not after a 【移动】, not outside the range', () => {
|
||||
for (const f of [[5, false, null], [6, true, TX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.duration, sk.bb.cost, sk.bb.cost_period, sk.bb['closur_s_2[add_cost_period].interval'], sk.bb.atk, sk.bb.def, sk.bb.block_cnt, sk.bb['attack@max_target'], sk.bb.cost_return],
|
||||
[30, elite ? 7 : 6, 15, 2, elite ? 0.55 : 0.4, elite ? 0.45 : 0.3, 1, 2, 0.4], label(f));
|
||||
// 角峰 (11,7) is in the x-4 of (10,6); 德克萨斯 (12,8) is outside it and outside her 3-3; 波登可 (9,9) far
|
||||
const others = [{ uid: 3, chessId: YAK, row: 11, col: 7 }, { uid: 4, chessId: TEXAS, row: 12, col: 8 }, { uid: 5, chessId: PODEGO, row: 12, col: 4 }];
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 1, others });
|
||||
const yak = h.unit(3), texas = h.unit(4), podego = h.unit(5);
|
||||
const log = dpLog(h);
|
||||
h.b.players[0].dp = 0;
|
||||
const atk0 = u.s.atk, yakDef = yak.s.def, yakBlock = yak.s.blockCnt;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||||
const t0 = h.b.time;
|
||||
approx(dpOf(h), sk.bb.cost, `${label(f)}: +${sk.bb.cost} at once`);
|
||||
approx(u.s.atk, atk0 * (1 + sk.bb.atk + (u.findBuff(RHODES_KEY)?.mods.atkPct ?? 0)) / (1 + (u.findBuff(RHODES_KEY)?.mods.atkPct ?? 0)), `${label(f)}: ATK +${sk.bb.atk * 100} %`, 1e-3);
|
||||
h.run(0.2);
|
||||
assert.deepEqual([t.s.blockCnt, yak.s.blockCnt], [3, yakBlock + 1], `${label(f)}: 援军 block +1`);
|
||||
approx(yak.s.def - yakDef, yak.base.def * sk.bb.def, `${label(f)}: 援军 DEF +${sk.bb.def * 100} %`, 1e-3);
|
||||
assert.equal(texas.findBuff(`${S2_KEY}:${u.id}`), null, `${label(f)}: not outside the x-4`);
|
||||
// an enemy blocked by 角峰 outside her 3-3? (11,7) is inside it; one blocked by the 指挥中心 and two more in range: 2 targets
|
||||
const es = [h.spawn('enemy_dummy', { pos: [10, 6] }), h.spawn('enemy_dummy', { pos: [9, 5] }), h.spawn('enemy_dummy', { pos: [11, 5] })];
|
||||
const n0 = h.hooksOf('attack').length;
|
||||
h.run(3);
|
||||
const atks = h.hooksOf('attack').slice(n0).filter((c) => c.attacker === u);
|
||||
assert.ok(atks.length >= 2 && atks.every((c) => c.targets.length === 2), `${label(f)}: 2 targets per attack (${atks.map((c) => c.targets.length)})`);
|
||||
for (const e of es) h.b.kill(e, null);
|
||||
// the refund: 角峰 (cost 19) redeployed on the x-4 ⇒ +8; 波登可 outside ⇒ nothing; a 【移动】 ⇒ nothing
|
||||
h.step();
|
||||
const d0 = dpOf(h);
|
||||
h.b.kill(yak, null);
|
||||
h.b.redeploy(yak, { free: true });
|
||||
approx(dpOf(h) - d0, Math.ceil(yak.base.cost * 0.4), `${label(f)}: ceil(40 % × ${yak.base.cost})`);
|
||||
const d1 = dpOf(h);
|
||||
h.b.kill(podego, null);
|
||||
h.b.redeploy(podego, { free: true });
|
||||
approx(dpOf(h) - d1, 0, `${label(f)}: outside the range — nothing`);
|
||||
assert.ok(h.b.moveRedeploy(yak, 9, 6), '【移动】');
|
||||
approx(dpOf(h) - d1, 0, `${label(f)}: a 【移动】 — nothing`);
|
||||
h.runUntil(() => !u.skill.active, 31);
|
||||
approx(h.b.time - t0, 30, `${label(f)}: 30 s`, 0.01);
|
||||
const periodic = log.filter(([tt]) => tt > t0 + 0.5 && tt <= t0 + 30.05).filter(([, d]) => d === 1);
|
||||
assert.equal(periodic.length, 15, `${label(f)}: 15 DP over the skill`);
|
||||
approx(periodic[0][0] - t0, 2, `${label(f)}: one per 2 s`, 0.06);
|
||||
assert.equal(t.findBuff(`${S2_KEY}:${u.id}`), null, `${label(f)}: the 援军 buff ends with it`);
|
||||
assert.equal(u.extraRangeKeys, null, `${label(f)}: no extra range after`);
|
||||
done(h);
|
||||
}
|
||||
// the enemies her 援军 block are her targets beyond her own range: 角峰 at (11,8) — out of her 3-3 ([1, 4]) — is in the
|
||||
// x-4 of a 指挥中心 at (10,7)
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: TX, skill: 1, base: [10, 7], others: [{ uid: 3, chessId: YAK, row: 11, col: 8 }] });
|
||||
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
|
||||
h.run(0.5);
|
||||
assert.equal(e.blockedBy, h.unit(3));
|
||||
assert.equal(hitsOn(h, u, e).length, 0, 'out of her range before');
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => hitsOn(h, u, e).length > 0, 4), 'hit while S2 runs');
|
||||
approx(hitsOn(h, u, e)[0].amount, u.s.atk * 1.5, 'with the trait ×1.5');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 Q.E.D.: 18 DP one per 1.66 s; attack interval 1.0 − 0.5 s; hits of attack@atk_scale × ATK with one 迟钝 stack each (4 % / 5 %, ≤ 10 stacks, one 3 s timer); one more target per 9 attacks (≤ 6); her range adds the 指挥中心\'s x-6 and her 援军\'s ranges', () => {
|
||||
for (const f of [[5, false, null], [6, true, TX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.duration, sk.bb.cost_period, sk.bb['closur_s_3[add_cost_period].interval'], sk.bb.base_attack_time, sk.bb['attack@atk_scale'], sk.bb['attack@slow_down'], sk.bb['attack@slow_down_time'], sk.bb['attack@max_stack_cnt'], sk.bb['attack@slow_down_max'], sk.bb.attack_trigger_cnt, sk.bb.max_trigger_cnt],
|
||||
[30, 18, 1.66, -0.5, elite ? 1.9 : 1.5, elite ? 0.05 : 0.04, 3, 10, elite ? 0.5 : 0.4, 9, 6], label(f));
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 2 });
|
||||
const log = dpLog(h);
|
||||
// eight enemies in her 3-3 (a flyer among them): enough for 1 + 6 targets
|
||||
const near = [[9, 4], [9, 5], [9, 6], [9, 7], [11, 4], [11, 5], [11, 6], [11, 7]].map((p, i) => h.spawn(i === 2 ? 'enemy_fly' : 'enemy_dummy', { pos: p }));
|
||||
h.run(1);
|
||||
h.b.players[0].dp = 0;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||||
const t0 = h.b.time;
|
||||
approx(u.s.interval, 0.5, `${label(f)}: interval 0.5 s`);
|
||||
let attacks = 0;
|
||||
const perAttack = [];
|
||||
h.b.on('attack', (c) => { if (c.attacker === u && u.skill.active) { attacks++; perAttack.push(c.targets.length); } });
|
||||
h.runUntil(() => attacks >= 9 + 1, 10);
|
||||
assert.ok(perAttack.slice(0, 9).every((n) => n === 1) && perAttack[9] === 2, `${label(f)}: one more target after 9 attacks (${perAttack})`);
|
||||
// the 迟钝 on one enemy: 1 stack per hit, capped
|
||||
const e0 = [...near].sort((a, b) => hitsOn(h, u, b).filter((c) => c.dmg.isSkill).length - hitsOn(h, u, a).filter((c) => c.dmg.isSkill).length)[0];
|
||||
const sl = e0.findBuff(SLOWDOWN);
|
||||
assert.ok(sl, `${label(f)}: 迟钝 on a hit enemy`);
|
||||
const skillHits = (e) => hitsOn(h, u, e).filter((c) => c.dmg.isSkill);
|
||||
const stacks = Math.min(10, skillHits(e0).length);
|
||||
approx(sl.data.value, Math.min(sk.bb['attack@slow_down_max'], sk.bb['attack@slow_down'] * stacks), `${label(f)}: ${stacks} stacks`);
|
||||
approx(e0.s.moveSpeed, e0.base.moveSpeed * (1 - sl.data.value), `${label(f)}: move speed ×(1 − v)`);
|
||||
for (const c of skillHits(e0).slice(0, 3)) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'], `${label(f)}: ${sk.bb['attack@atk_scale'] * 100} % ATK`);
|
||||
h.runUntil(() => !u.skill.active, 31);
|
||||
approx(h.b.time - t0, 30, `${label(f)}: 30 s`, 0.01);
|
||||
assert.equal(Math.max(...perAttack), 7, `${label(f)}: at most 1 + 6 targets`);
|
||||
approx(log.reduce((a, [tt, d]) => a + (tt > t0 - 1e-9 && tt < t0 + 30.05 ? d : 0), 0), 18, `${label(f)}: 18 DP`);
|
||||
approx(log.find(([tt]) => tt > t0 - 1e-9)[0] - t0, 1.66, `${label(f)}: the first after 1.66 s`, 0.06);
|
||||
// the cap after 10 hits, one timer: 3 s without a hit clears every stack
|
||||
const capped = near.find((e) => skillHits(e).length >= 10);
|
||||
assert.ok(capped);
|
||||
h.run(0.1);
|
||||
h.b.kill(u, null);
|
||||
h.run(3.1);
|
||||
assert.equal(capped.findBuff(SLOWDOWN), null, `${label(f)}: gone 3 s after the last hit`);
|
||||
done(h);
|
||||
void t;
|
||||
}
|
||||
// her range adds the 指挥中心's x-6: (10,8) is outside her 3-3 (cols 4–7), inside the x-6 of (10,6) ([0, 2])
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: TX, skill: 2 });
|
||||
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
h.run(2);
|
||||
assert.equal(hitsOn(h, u, far).length, 0, 'out of her range before');
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => hitsOn(h, u, far).length > 0, 3), 'hit while S3 runs');
|
||||
assert.ok(u.extraRangeKeys.includes(tk(10, 8)) && u.extraRangeKeys.includes(tk(8, 6)), 'the x-6 in her range');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('T2 极限调度: from the battle start the 【罗德岛】 operators of her player (herself too) ATK +4 % (TAC-X stage 3 +8 %) for the whole battle; others not', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const tb = talentOf(tier, elite, mod, 1).bb;
|
||||
assert.deepEqual(tb, tier === 6 && elite && mod === TX ? { cost: -5, atk: 0.08 } : { cost: -3, atk: 0.04 }, label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 3, chessId: PODEGO, row: 12, col: 3 }, { uid: 4, chessId: YAK, row: 11, col: 3 }] });
|
||||
const podego = h.unit(3), yak = h.unit(4);
|
||||
assert.equal(podego.def.raw.nationId, 'rhodes');
|
||||
for (const a of [u, podego]) assert.equal(a.findBuff(RHODES_KEY)?.mods.atkPct, tb.atk, `${label(f)}: ${a.defId}`);
|
||||
assert.equal(yak.findBuff(RHODES_KEY), null, `${label(f)}: 角峰 is no 罗德岛 operator`);
|
||||
h.b.kill(u, null);
|
||||
h.run(1);
|
||||
assert.equal(podego.findBuff(RHODES_KEY)?.mods.atkPct, tb.atk, `${label(f)}: kept while she is down (携带)`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('TAC-X “给自己的小奖杯”: damage on one of her 援军 from an enemy it blocks ×0.85 (stages 1 and 3); not without the module', () => {
|
||||
for (const f of [[5, false, null], [5, true, null], [5, true, TX], [6, true, TX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0 });
|
||||
const guard = elite && mod === TX ? modOf(tier, TX).talentChanges.find((x) => x.talentIndex === -1).bb.damage_scale : 1;
|
||||
if (elite && mod === TX) assert.equal(guard, 0.85);
|
||||
const e = h.spawn('enemy_atk', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === e && c.target === t), 5), `${label(f)}: hit by the enemy it blocks`);
|
||||
const c = h.hooksOf('damaged').find((x) => x.source === e && x.target === t);
|
||||
approx(c.amount, Math.max(2000 - t.s.def, 100) * guard, `${label(f)}: ×${guard}`);
|
||||
done(h);
|
||||
void u;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
// test/content/op_gdglow.test.js — the 自选 operator kit of 澄闪 (char_377_gdglow, 6★ 驭械术师; kit
|
||||
// server/sim/content/kits/ops/op-gdglow.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in every
|
||||
// form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, FUN-X “辛劳之翼” or
|
||||
// FUN-Y 梦想终将实现 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_gdglow.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
import { BLAST_RADIUS, DRONE_TAG, BLAST_TAG } from '../../server/sim/content/kits/ops/op-gdglow.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 GD = 'char_377_gdglow';
|
||||
const FORMS = BACKUPS.units[GD].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const FX = 'uniequip_002_gdglow', FY = 'uniequip_003_gdglow';
|
||||
const S1 = 'skchr_gdglow_1', S2 = 'skchr_gdglow_2', S3 = 'skchr_gdglow_3';
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const talentOf = (tier, elite, mod, i) => modOf(tier, elite ? mod : null)?.talentChanges?.find((t) => t.talentIndex === i) ?? formOf(tier, elite).talents.find((t) => t.index === i);
|
||||
const traitOf = (tier, elite, mod) => (elite && mod ? modOf(tier, mod).traitOverride : formOf(tier, elite).trait);
|
||||
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_res: dummy('enemy_res', { res: 50 }), enemy_weak: dummy('enemy_weak', { hp: 10 }) };
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, FX, FY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** 澄闪 as uid 1 at (row, col) facing RIGHT. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, others = [], seed = 5 } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'attack'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: GD, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
const mine = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u);
|
||||
const droneHits = (h, u, e = null) => mine(h, u).filter((c) => c.dmg.tags?.includes(DRONE_TAG) && (!e || c.target === e));
|
||||
|
||||
test('澄闪 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, 驭械术师 (ranged arts, hits air, blocks 1; the trait ramp of the form — FUN-X init 0.35, FUN-Y cap 1.2), ground-targetable; S1 DEFAULT, S2 at full SP (AUTO on herself), S3 GDGLOW_SKILL_2; 精准导流 15 (FUN-Y stage 3 20)', () => {
|
||||
assert.equal(OPERATOR_KITS[GD], KITS[GD]);
|
||||
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, sk = form.skills[skill];
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [GD, SLOT[tier], sk.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), 100 + (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], [1, 'ranged', true, 'arts', 'funnel', 1.3], `${label(f)}: 驭械术师`);
|
||||
const tb = traitOf(tier, elite, mod).bb;
|
||||
assert.deepEqual(u.profile.funnel, { init: tb.init_atk_scale, delta: tb.delta_atk_scale, max: tb.max_atk_scale }, `${label(f)}: the trait ramp`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
assert.deepEqual([u.skill.rule, u.skill.kind], [[sk.trigger.rule, 'SP_FULL', 'GDGLOW_SKILL_2'][skill], ['duration', 'toggle', 'duration'][skill]], `${label(f)}: S${skill + 1}`);
|
||||
assert.equal(u.s.resIgnoreFlat, talentOf(tier, elite, mod, 1).bb.magic_resist_penetrate_fixed, `${label(f)}: 精准导流`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
assert.deepEqual([skillOf(5, false, S1).trigger.rule, skillOf(5, false, S2).trigger.rule, skillOf(5, false, S2).skillType], ['DEFAULT', 'DEFAULT', 'AUTO']);
|
||||
assert.deepEqual([modOf(5, FX).attr, modOf(6, FX).attr, modOf(5, FY).attr, modOf(6, FY).attr], [{ maxHp: 90, atk: 22 }, { maxHp: 125, atk: 38 }, { atk: 30, aspd: 5 }, { atk: 45, aspd: 7 }]);
|
||||
assert.deepEqual([talentOf(6, true, FY, 1).bb.magic_resist_penetrate_fixed, talentOf(6, true, FX, 0).bb['attack@atk_scale_2'], talentOf(5, true, FX, 0).bb['attack@atk_scale_2']], [20, 3.6, 3]);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 澄闪 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(GD));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(GD), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: GD, skillIndex: 2, uniEquipId: FY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: GD, skillIndex: 2, uniEquipId: FY } } });
|
||||
});
|
||||
|
||||
test('her normal attack (no skill): one drone hit per attack — init, +delta per hit on the same target, up to the cap (FUN-X from 0.35, FUN-Y to 1.2); 精准导流 lowers the RES it meets', () => {
|
||||
for (const f of [[5, false, null], [5, true, FX], [6, true, FY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
const e = h.spawn('enemy_res', { pos: [10, 6] });
|
||||
h.run(12);
|
||||
const hits = mine(h, u).filter((c) => c.target === e && c.dmg.isAttack);
|
||||
assert.ok(hits.length >= 8, `${label(f)}: ${hits.length} attacks`);
|
||||
const tb = traitOf(tier, elite, mod).bb, pen = talentOf(tier, elite, mod, 1).bb.magic_resist_penetrate_fixed;
|
||||
hits.slice(0, 8).forEach((c, i) => {
|
||||
const ramp = Math.min(tb.max_atk_scale, tb.init_atk_scale + i * tb.delta_atk_scale);
|
||||
approx(c.amount, u.s.atk * ramp * (1 - (50 - pen) / 100), `${label(f)}: attack ${i + 1} at ${ramp}`);
|
||||
assert.equal(c.type, 'arts');
|
||||
});
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 火花四溅 (DEFAULT: on her attack with an enemy in range): 25 s, ATK +atk, ASPD +aspd, no attack of her own; 2 drones lock her first target (in range or not afterwards) and hit it every attack interval — each its own ramp, arts 技能伤害 — and take her next first target once it falls; 缴械 stops nothing', () => {
|
||||
for (const f of [[5, false, null], [6, true, FX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.attack_speed, sk.bb['attack@cnt']], [25, elite ? 0.32 : 0.26, elite ? 38 : 29, 1], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
u.skill.gainSp(999);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, `${label(f)}: no enemy, no cast`);
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `${label(f)}: cast as she is about to attack`);
|
||||
const t0 = h.b.time;
|
||||
approx(u.s.aspd, u.base.aspd + sk.bb.attack_speed, `${label(f)}: ASPD +${sk.bb.attack_speed}`);
|
||||
h.b.applyStatus(u, 'disarm', { duration: 30, source: u, force: true });
|
||||
h.run(3);
|
||||
const hits = droneHits(h, u, a);
|
||||
assert.ok(hits.length >= 4, `${label(f)}: drone hits while 缴械 (${hits.length})`);
|
||||
assert.ok(hits.every((c) => c.dmg.isSkill && !c.dmg.isAttack && c.type === 'arts'), `${label(f)}: arts 技能伤害, no normal attack`);
|
||||
const f0 = u.profile.funnel;
|
||||
approx(hits[0].amount, u.s.atk * f0.init, `${label(f)}: the first at init`);
|
||||
approx(hits[1].amount, u.s.atk * f0.init, `${label(f)}: the second drone too`);
|
||||
approx(hits[2].amount, u.s.atk * (f0.init + f0.delta), `${label(f)}: +delta`);
|
||||
approx(hits[2].t - hits[0].t, u.s.interval, `${label(f)}: one hit per attack interval each`, 0.05);
|
||||
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u && c.t > t0).length, 0, `${label(f)}: no attack of her own`);
|
||||
// locked: the enemy walks out of her range — still hit
|
||||
a.x = 12; a.y = 12;
|
||||
const n1 = droneHits(h, u, a).length;
|
||||
h.run(2);
|
||||
assert.ok(droneHits(h, u, a).length > n1, `${label(f)}: kept out of range`);
|
||||
// it falls: the drones take her first target
|
||||
const b = h.spawn('enemy_dummy', { pos: [11, 5] });
|
||||
h.b.kill(a, null);
|
||||
h.run(1.5);
|
||||
assert.ok(droneHits(h, u, b).length >= 2, `${label(f)}: the next first target`);
|
||||
h.runUntil(() => !u.skill.active, 25);
|
||||
approx(h.b.time - t0, 25, `${label(f)}: 25 s`, 0.01);
|
||||
assert.equal(u.mem.gdDrones, null, `${label(f)}: the drones back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 电流翻涌 (AUTO, at full SP — no enemy needed): on for good, range 3-18, ATK +atk, 2 drones on an enemy of the 3-18 outside her 3-1', () => {
|
||||
for (const f of [[5, false, null], [6, true, FY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb['attack@cnt'], sk.rangeId, sk.initSp, sk.spCost], [-1, elite ? 0.45 : 0.32, 1, '3-18', 0, elite ? 70 : 75], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||||
h.run(sk.spCost - 0.5);
|
||||
assert.equal(u.skill.activations, 0, `${label(f)}: not before its SP is full`);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `${label(f)}: on at full SP with no enemy`);
|
||||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `${label(f)}: 3-18`);
|
||||
// (12,4) = [2, 0]: in the 3-18, not in the 3-1
|
||||
const e = h.spawn('enemy_fly', { pos: [12, 4] });
|
||||
h.run(3);
|
||||
const hits = droneHits(h, u, e);
|
||||
assert.ok(hits.length >= 4, `${label(f)}: the drones on it (${hits.length})`);
|
||||
approx(hits[0].amount, u.s.atk * u.profile.funnel.init, `${label(f)}: ATK +${sk.bb.atk * 100} % (live ATK)`);
|
||||
h.run(200);
|
||||
assert.ok(u.skill.active, `${label(f)}: 持续时间无限`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 澄净闪耀 (GDGLOW_SKILL_2: an enemy anywhere): 30 s, no attack of her own, the whole field, ATK +atk, 3 drones, each hit 0.5 s 停顿', () => {
|
||||
for (const f of [[5, false, null], [6, true, FY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb['attack@cnt'], sk.bb['attack@sluggish']], [30, elite ? 0.55 : 0.4, 2, 0.5], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
u.skill.gainSp(999);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, `${label(f)}: no enemy, no cast`);
|
||||
const e = h.spawn('enemy_dummy', { pos: [12, 10] }); // far out of her 3-1
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `${label(f)}: cast with an enemy anywhere`);
|
||||
const t0 = h.b.time;
|
||||
h.run(0.2);
|
||||
assert.equal(u.mem.gdDrones.length, 3, `${label(f)}: 3 drones`);
|
||||
h.run(3);
|
||||
const hits = droneHits(h, u, e);
|
||||
assert.ok(hits.length >= 6, `${label(f)}: hit across the field (${hits.length})`);
|
||||
const sl = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'sluggish' && c.target === e);
|
||||
assert.ok(sl.length >= hits.length - 1, `${label(f)}: 停顿 per hit`);
|
||||
approx(sl[0].duration, 0.5, `${label(f)}: 0.5 s`);
|
||||
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u && c.t > t0).length, 0, `${label(f)}: 停止攻击`);
|
||||
h.runUntil(() => !u.skill.active, 30);
|
||||
approx(h.b.time - t0, 30, `${label(f)}: 30 s`, 0.01);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 信标的愤怒: per drone 1.5 % × its stacks (from 1, +1 per miss, sure past 40): ~10 % of the drone attacks; the blast — atk_scale_2 × ATK arts within 1.1 of the target (air too), no ramp (FUN-X stage 3 360 %), 停顿 under S3 — sends the drone back and restarts its count; reset at a deployment', () => {
|
||||
for (const f of [[5, false, null], [6, true, FX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const tb = talentOf(tier, elite, mod, 0).bb;
|
||||
assert.deepEqual([tb['attack@prob'], tb['attack@atk_scale_2'], tb['attack@max_stack_cnt']], [0.015, elite && mod === FX && tier === 6 ? 3.6 : 3, 40], label(f));
|
||||
// a sure blast: both drones past the cap
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 6] }), near = h.spawn('enemy_fly', { pos: [11, 6] }), far = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
u.mem.gdStacks = [41, 41, 41];
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => mine(h, u).some((c) => c.dmg.tags?.includes(BLAST_TAG)), 3), `${label(f)}: blast`);
|
||||
const blast = mine(h, u).filter((c) => c.dmg.tags?.includes(BLAST_TAG));
|
||||
const hitT = new Set(blast.map((c) => c.target));
|
||||
assert.ok(hitT.has(e) && hitT.has(near) && !hitT.has(far), `${label(f)}: within ${BLAST_RADIUS} of the target (air too), not 2 tiles away`);
|
||||
for (const c of blast) {
|
||||
approx(c.amount, u.s.atk * tb['attack@atk_scale_2'], `${label(f)}: ${tb['attack@atk_scale_2'] * 100} % ATK, no ramp`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['arts', true]);
|
||||
}
|
||||
assert.ok(h.hooksOf('statusApplied').some((c) => c.source === u && c.status === 'sluggish' && c.target === far) === false, 'no 停顿 outside');
|
||||
assert.ok(h.hooksOf('statusApplied').some((c) => c.source === u && c.status === 'sluggish' && c.target === near), `${label(f)}: S3's 停顿 on the blast`);
|
||||
assert.ok(u.mem.gdStacks.slice(0, 3).some((k) => k <= 2), `${label(f)}: a count restarted`);
|
||||
h.b.retreat(u);
|
||||
h.b.redeploy(u, { free: true });
|
||||
assert.deepEqual(u.mem.gdStacks, [], `${label(f)}: reset at a deployment`);
|
||||
done(h);
|
||||
}
|
||||
// the macro rate: ~10 % of the drone attacks (the pseudo-random distribution's 1 / 9.91)
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: FY, skill: 1 });
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
u.skill.gainSp(999);
|
||||
h.run(600);
|
||||
const n = droneHits(h, u).length, b = new Set(mine(h, u).filter((c) => c.dmg.tags?.includes(BLAST_TAG)).map((c) => c.t)).size;
|
||||
assert.ok(n > 800, `${n} drone attacks`);
|
||||
assert.ok(b / n > 0.08 && b / n < 0.125, `${b} / ${n} blasts ≈ 10 %`);
|
||||
done(h);
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
// test/content/op_sleach.test.js — the 自选 operator kit of 琴柳 (char_479_sleach, 6★ 执旗手; kit
|
||||
// server/sim/content/kits/ops/op-sleach.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in every
|
||||
// form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, BEA-X 牧人的歌 or
|
||||
// BEA-Y “友谊万岁” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json; the fidelity checklist
|
||||
// of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_sleach.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
import { FLAG_FLIGHT, FLAG_ALLY, FLAG_ENEMY, S2_KEY, FRONT_KEY, CAMOU_KEY } from '../../server/sim/content/kits/ops/op-sleach.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 SL = 'char_479_sleach';
|
||||
const FORMS = BACKUPS.units[SL].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const BX = 'uniequip_002_sleach', BY = 'uniequip_003_sleach';
|
||||
const S1 = 'skcom_assist_cost[3]', S2 = 'skchr_sleach_2', S3 = 'skchr_sleach_3';
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
/** The talents of a form with the module's changes (the composed record's). */
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const ch = modOf(tier, elite ? mod : null)?.talentChanges?.find((t) => t.talentIndex === i);
|
||||
return ch ?? formOf(tier, elite).talents.find((t) => t.index === i);
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }) };
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, BX, BY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
const TEXAS = 'chess_char_1_08_a', YAK = 'chess_char_1_02_a', PROVE = 'chess_char_1_07_a';
|
||||
|
||||
/** A battle with 琴柳 as uid 1 at (row, col) facing RIGHT, plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5, flags = {} } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999, ...flags }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'deploy', 'death', 'attack'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: SL, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
const dpOf = (h) => h.b.players[0].dp;
|
||||
|
||||
test('琴柳 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 1-1, 执旗手 (blocks 1, melee physical, ground-only), ground-targetable, 维多利亚; the three triggers SP_FULL (the 执旗手 row)', () => {
|
||||
assert.equal(OPERATOR_KITS[SL], KITS[SL]);
|
||||
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, sk = form.skills[skill];
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [SL, SLOT[tier], sk.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], [1, 'melee', false, 'phys', 'bearer', 1.3], `${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.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
assert.deepEqual([u.skill.rule, u.skill.spCost, u.skill.kind, sk.trigger.rawRule], ['SP_FULL', sk.spCost, 'duration', 'ALWAYS'], `${label(f)}: trigger / SP`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the form numbers (zh_CN): E2 Lv1 1339 / 458 / 311, E2 Lv60 1668 / 516 / 348; BEA-X +150 / +48 → +200 / +68 HP / ATK,
|
||||
// BEA-Y +150 / +45 → +200 / +70 HP / DEF
|
||||
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1339, 458, 1668, 516]);
|
||||
assert.deepEqual([modOf(5, BX).attr, modOf(6, BX).attr, modOf(5, BY).attr, modOf(6, BY).attr], [{ maxHp: 150, atk: 48 }, { maxHp: 200, atk: 68 }, { maxHp: 150, def: 45 }, { maxHp: 200, def: 70 }]);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 琴柳 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(SL));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(SL), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: SL, skillIndex: 2, uniEquipId: BY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: SL, skillIndex: 2, uniEquipId: BY } } });
|
||||
});
|
||||
|
||||
test('S1 支援号令·γ型: cast as soon as its SP is full (no enemy needed), 8 s, no attack, blocks 0 meanwhile; +1 DP every 0.44 s from 0.44 s, 18 in all', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, BX]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 2, chessId: YAK, row: 10, col: 7 }] });
|
||||
assert.deepEqual([sk.duration, sk.bb.value, sk.bb.interval, sk.bb.cost, sk.initSp, sk.spCost], [8, 18, 0.44, 1, elite ? 12 : 11, elite ? 29 : 32], `T${tier}`);
|
||||
h.run(sk.spCost - sk.initSp - 0.5);
|
||||
assert.equal(u.skill.activations, 0, `T${tier}: not before its SP is full`);
|
||||
h.b.players[0].dp = 0;
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast at full SP with no enemy on the field`);
|
||||
const t0 = h.b.time;
|
||||
let attacksOn = 0;
|
||||
h.b.on('attack', (c) => { if (c.attacker === u && u.skill.active) attacksOn++; });
|
||||
assert.equal(u.s.blockCnt, 0, `T${tier}: 执旗手 — block 0 while it runs`);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.runUntil(() => dpOf(h) >= 1, 1);
|
||||
approx(h.b.time - t0, 0.44, `T${tier}: the first DP after one interval`, 0.1);
|
||||
h.runUntil(() => !u.skill.active, 9);
|
||||
approx(h.b.time - t0, 8, `T${tier}: 8 s`, 0.01);
|
||||
approx(dpOf(h), 18, `T${tier}: 18 DP`);
|
||||
assert.equal(attacksOn, 0, `T${tier}: 停止攻击`);
|
||||
h.run(1);
|
||||
approx(dpOf(h), 18, `T${tier}: nothing after`);
|
||||
assert.equal(u.s.blockCnt, 1, `T${tier}: block back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 信仰传承: the flag goes to the operator with the lowest HP share on the x-1 (not her, not a 孤立 one, not out of range): DEF +def and 生命回复速度 ATK × ratio there (through 禁疗), T1\'s area follows the flag; 20 DP over 15 s; back to her after', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, BY]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
const others = [
|
||||
{ uid: 2, chessId: YAK, row: 10, col: 6 }, // [0, 1]: in range, 80 %
|
||||
{ uid: 3, chessId: TEXAS, row: 11, col: 4 }, // [1, -1]: in range, 40 % — the target
|
||||
{ uid: 4, chessId: PROVE, row: 12, col: 7 }, // [2, 2]: outside the x-1, 10 %
|
||||
{ uid: 5, chessId: 'chess_char_1_09_a', row: 9, col: 5 }, // [-1, 0]: in range, 5 % but 孤立 — never chosen
|
||||
];
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, row: 10, col: 5, others });
|
||||
const yak = h.unit(2), texas = h.unit(3), prove = h.unit(4);
|
||||
assert.deepEqual([sk.duration, sk.bb.value, sk.bb['sleach_s_2[cost].interval'], sk.rangeId, sk.bb.def, sk.bb.atk_to_hp_recovery_ratio], [15, 20, 0.75, 'x-1', elite ? 0.35 : 0.22, elite ? 0.35 : 0.22], `T${tier}`);
|
||||
yak.hp = yak.s.maxHp * 0.8; texas.hp = texas.s.maxHp * 0.4; prove.hp = prove.s.maxHp * 0.1;
|
||||
u.hp = u.s.maxHp * 0.05; // her own share never makes her the target
|
||||
const iso = h.unit(5);
|
||||
iso.hp = iso.s.maxHp * 0.05;
|
||||
h.b.addBuff(iso, { key: 'test:isolated', flags: { isolated: true } });
|
||||
h.b.addBuff(texas, { key: 'test:noheal', flags: { noHeal: true } });
|
||||
h.b.players[0].dp = 0;
|
||||
const defBefore = texas.s.def;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||||
const t0 = h.b.time;
|
||||
assert.deepEqual(u.mem.sleachFlag && [u.mem.sleachFlag.r, u.mem.sleachFlag.c], [11, 4], `T${tier}: the flag on 德克萨斯's tile`);
|
||||
h.run(0.3);
|
||||
const b = texas.findBuff(`${S2_KEY}:${u.id}`);
|
||||
assert.ok(b, `T${tier}: the flag effect on 德克萨斯`);
|
||||
approx(b.mods.defPct, sk.bb.def, `T${tier}: DEF +${sk.bb.def * 100} %`);
|
||||
approx(b.mods.hpRegen, u.s.atk * sk.bb.atk_to_hp_recovery_ratio, `T${tier}: 生命回复速度 = ATK × ratio`);
|
||||
approx(texas.s.def - defBefore, texas.base.def * sk.bb.def, `T${tier}: DEF applied (直接乘算)`, 1e-3);
|
||||
const hp0 = texas.hp;
|
||||
h.run(2);
|
||||
assert.ok(texas.hp > hp0 + 1.5 * u.s.atk * sk.bb.atk_to_hp_recovery_ratio, `T${tier}: it regenerates through 禁疗 (${hp0} → ${texas.hp})`);
|
||||
assert.equal(yak.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: only the operator on the flag's tile`);
|
||||
assert.equal(prove.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: the 10 % operator outside the x-1 is never chosen`);
|
||||
// T1's 3×3 follows the flag: 德克萨斯 (on it) and 琴柳 ([−1, +1] of it) are inside, 角峰 ([−1, +2]) is not
|
||||
const asp = talentOf(tier, elite, mod, 0).bb['sleach_t_1[ally].attack_speed'];
|
||||
assert.equal(texas.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 德克萨斯 in the flag's area`);
|
||||
assert.equal(u.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 琴柳 next to the flag`);
|
||||
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`), null, `T${tier}: 角峰 outside the flag's area`);
|
||||
h.runUntil(() => !u.skill.active, 16);
|
||||
approx(h.b.time - t0, 15, `T${tier}: 15 s`, 0.01);
|
||||
approx(dpOf(h), 20, `T${tier}: 20 DP`);
|
||||
assert.equal(u.mem.sleachFlag, null, `T${tier}: the flag back`);
|
||||
assert.equal(texas.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: the effect ends with the skill`);
|
||||
h.run(0.5);
|
||||
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 角峰 next to her again`);
|
||||
done(h);
|
||||
}
|
||||
// nobody else on the x-1: the flag stays on her tile — she has DEF / 生命回复速度 there
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: BX, skill: 1 });
|
||||
const sk = skillOf(6, true, S2);
|
||||
u.skill.gainSp(999);
|
||||
h.run(0.3);
|
||||
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 5], 'her own tile');
|
||||
approx(u.findBuff(`${S2_KEY}:${u.id}`)?.mods.defPct ?? 0, sk.bb.def, 'on herself');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 光辉旗帜: +10 DP at once; the flag on the ground tile of the enemy ranked first in the 2-1 (never a flyer): 停顿 + 脆弱 on its 3×3 at once, the strike 1 s later — atk_scale × ATK physical and 3 s 晕眩 to ground and air enemies of the 3×3; no ground enemy → her own tile', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, BX]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, row: 10, col: 4 });
|
||||
assert.deepEqual([sk.duration, sk.bb.cost, sk.bb.atk_scale, sk.bb.stun, sk.bb['debuff.damage_scale'], sk.rangeId], [10, 10, elite ? 2.4 : 2, 3, elite ? 0.25 : 0.2, '2-1'], `T${tier}`);
|
||||
// in the 2-1 (facing RIGHT from 10,4): (10,6) [0,2] and (11,5) [1,1]; a taunt makes (10,6) the one her targeting ranks
|
||||
// first (the highest 仇恨); its 3×3 (rows 9–11, cols 5–7) holds (11,5) and the flyer at (9,6), not (10,8)
|
||||
const first = h.spawn('enemy_dummy', { pos: [10, 6] }), second = h.spawn('enemy_dummy', { pos: [11, 5] });
|
||||
h.b.addBuff(first, { key: 'test:taunt', mods: { taunt: 1 } });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 6] });
|
||||
const out = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
h.b.players[0].dp = 0;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||||
const t0 = h.b.time;
|
||||
approx(dpOf(h), 10, `T${tier}: +10 DP at once`);
|
||||
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 6], `T${tier}: the first-ranked ground enemy's tile`);
|
||||
h.run(0.3);
|
||||
const inArea = [first, second, fly];
|
||||
for (const e of inArea) {
|
||||
assert.ok(e.findBuff('sluggish'), `T${tier}: ${e.defId} 停顿 before the flag lands`);
|
||||
approx(e.findBuff('fragile')?.data?.value ?? 0, sk.bb['debuff.damage_scale'], `T${tier}: ${e.defId} 脆弱`);
|
||||
assert.equal(e.findBuff('stun'), null, `T${tier}: no 晕眩 yet`);
|
||||
}
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u).length, 0, `T${tier}: no damage before it lands`);
|
||||
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === u), 2));
|
||||
approx(h.b.time - t0, FLAG_FLIGHT, `T${tier}: it lands ${FLAG_FLIGHT} s after the throw`, 0.05);
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === u);
|
||||
const struck = new Set(hits.map((c) => c.target));
|
||||
for (const e of inArea) {
|
||||
assert.ok(struck.has(e), `T${tier}: ${e.defId} struck (ground and air)`);
|
||||
const c = hits.find((x) => x.target === e);
|
||||
approx(c.amount, u.s.atk * sk.bb.atk_scale * (1 + sk.bb['debuff.damage_scale']), `T${tier}: ${sk.bb.atk_scale * 100} % ATK × the 脆弱`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true]);
|
||||
const st = h.hooksOf('statusApplied').find((x) => x.source === u && x.status === 'stun' && x.target === e);
|
||||
approx(st?.duration ?? 0, sk.bb.stun, `T${tier}: ${sk.bb.stun} s 晕眩`);
|
||||
}
|
||||
assert.ok(!struck.has(out) && !out.findBuff('sluggish'), `T${tier}: nothing outside the 3×3`);
|
||||
h.runUntil(() => !u.skill.active, 11);
|
||||
approx(h.b.time - t0, 10, `T${tier}: 10 s`, 0.01);
|
||||
assert.equal(u.mem.sleachFlag, null, `T${tier}: the flag back`);
|
||||
done(h);
|
||||
}
|
||||
// only a flyer in the 2-1, then nobody: her own tile
|
||||
for (const pos of [[10, 6], null]) {
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: BY, skill: 2, row: 10, col: 4 });
|
||||
if (pos) h.spawn('enemy_fly', { pos });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||||
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 4], pos ? 'a flyer is never the target' : 'no enemy: her tile');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 不退之旗 in every form: operators of the 3×3 around her ASPD +10 (BEA-X stage 3 +13) — 孤立 ones too, summons not —, enemies there (ground and air) ASPD −10 (−13); nothing outside, nothing once she left', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const tb = talentOf(tier, elite, mod, 0).bb;
|
||||
const others = [{ uid: 2, chessId: YAK, row: 11, col: 6 }, { uid: 3, chessId: TEXAS, row: 9, col: 4 }, { uid: 4, chessId: PROVE, row: 10, col: 7 }];
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, others });
|
||||
const yak = h.unit(2), texas = h.unit(3), prove = h.unit(4);
|
||||
h.b.addBuff(texas, { key: 'test:isolated', flags: { isolated: true } });
|
||||
const tok = h.b.spawnToken('p1', 'token_10000_silent_healrb', 11, 4);
|
||||
const e1 = h.spawn('enemy_dummy', { pos: [10, 6] }), e2 = h.spawn('enemy_fly', { pos: [9, 5] }), e3 = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.run(0.5);
|
||||
const a = tb['sleach_t_1[ally].attack_speed'], en = tb['sleach_t_1[enemy].attack_speed'];
|
||||
assert.deepEqual([a, en], tier === 6 && elite && mod === BX ? [13, -13] : [10, -10], label(f));
|
||||
for (const x of [u, yak, texas]) assert.equal(x.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, a, `${label(f)}: ${x.defId} +${a}`);
|
||||
assert.equal(prove.findBuff(`${FLAG_ALLY}:${u.id}`), null, `${label(f)}: two tiles away`);
|
||||
assert.ok(tok && !tok.findBuff(`${FLAG_ALLY}:${u.id}`), `${label(f)}: no summon`);
|
||||
for (const e of [e1, e2]) assert.equal(e.findBuff(`${FLAG_ENEMY}:${u.id}`)?.mods.aspd, en, `${label(f)}: ${e.defId} ${en}`);
|
||||
assert.equal(e3.findBuff(`${FLAG_ENEMY}:${u.id}`), null, `${label(f)}: the enemy two tiles away`);
|
||||
approx(e1.s.aspd, 100 + en, `${label(f)}: enemy ASPD`);
|
||||
h.b.retreat(u);
|
||||
h.run(0.5);
|
||||
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`), null, `${label(f)}: gone with her`);
|
||||
assert.equal(e1.findBuff(`${FLAG_ENEMY}:${u.id}`), null, `${label(f)}: gone with her (enemy)`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 精神感召: from her deployment the operators of her player off the field cost −2 until the next operator deployment; BEA-Y stage 3 adds +2 DP when that operator is 近战位 (once) — nothing at stage 1 or for a 远程位 one', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const bonus = tier === 6 && elite && mod === BY ? 2 : 0;
|
||||
if (bonus) assert.deepEqual(modOf(6, BY).talentChanges.find((t) => t.talentIndex === -1).bb, { cost: 2 });
|
||||
// she deploys last (col 7 after cols 3 / 4): no operator waits, so the effect stays; a knocked-out one gets the cut
|
||||
for (const [chessId, melee] of [[TEXAS, true], [PROVE, false]]) {
|
||||
const { h } = field({ tier, elite, mod, skill: 0, row: 10, col: 7, others: [{ uid: 2, chessId, row: 10, col: 3 }, { uid: 3, chessId: YAK, row: 11, col: 4 }] });
|
||||
const op = h.unit(2), cost = op.base.cost;
|
||||
h.b.kill(op);
|
||||
assert.equal(op.base.cost, cost - 2, `${label(f)} ${chessId}: −2 while it waits`);
|
||||
h.b.players[0].dp = 20;
|
||||
assert.ok(h.b.redeploy(op, { free: false }), 'redeploys');
|
||||
approx(dpOf(h), 20 - (cost - 2) + (melee ? bonus : 0), `${label(f)} ${chessId}: paid ${cost - 2}${melee && bonus ? ', +2 DP (近战位)' : ''}`);
|
||||
assert.equal(op.base.cost, cost, `${label(f)} ${chessId}: the cost back once used`);
|
||||
h.b.kill(op);
|
||||
assert.equal(op.base.cost, cost, `${label(f)} ${chessId}: used up — no cut on the next knock-out`);
|
||||
h.b.players[0].dp = 20;
|
||||
h.b.redeploy(op, { free: false });
|
||||
approx(dpOf(h), 20 - cost, `${label(f)} ${chessId}: and no second +DP`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// an operator deploying after her in the battle-start deployment uses it up at once (free: nothing saved); her leaving
|
||||
// ends a pending one
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: BY, skill: 0, row: 10, col: 3, others: [{ uid: 2, chessId: YAK, row: 10, col: 6 }] });
|
||||
const yk = h.unit(2), cost = yk.base.cost;
|
||||
approx(dpOf(h), 10 + 2, 'BEA-Y stage 3: 角峰 (近战位) deployed right after her: +2 DP at the battle start');
|
||||
h.b.kill(yk);
|
||||
assert.equal(yk.base.cost, cost, 'used up by the battle-start deployment');
|
||||
h.b.retreat(u);
|
||||
h.b.redeploy(u, { free: true });
|
||||
assert.equal(yk.base.cost, cost - 2, 'her redeploy: the waiting 角峰 −2 again');
|
||||
h.b.retreat(u);
|
||||
assert.equal(yk.base.cost, cost, 'her leaving ends it');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('BEA-X “牧人的歌”: while her skill runs the operator in front of her blocks +1 (stages 1 and 3), not before / after, not without the module; BEA-Y “友谊万岁”: 迷彩 while her skill runs', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, row: 10, col: 5, others: [{ uid: 2, chessId: YAK, row: 10, col: 6 }, { uid: 3, chessId: TEXAS, row: 11, col: 5 }] });
|
||||
const front = h.unit(2), side = h.unit(3);
|
||||
const bx = elite && mod === BX, by = elite && mod === BY;
|
||||
if (bx) assert.deepEqual(u.def.raw.trait.bb, { block_cnt: 1 }, label(f));
|
||||
const base = front.s.blockCnt;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||||
h.run(0.3);
|
||||
assert.equal(front.s.blockCnt, base + (bx ? 1 : 0), `${label(f)}: the operator in front`);
|
||||
assert.equal(!!side.findBuff(`${FRONT_KEY}:${u.id}`), false, `${label(f)}: not the one beside her`);
|
||||
assert.equal(!!u.s.flags.camou, by, `${label(f)}: 迷彩 while it runs`);
|
||||
assert.equal(!!u.findBuff(CAMOU_KEY), by);
|
||||
h.runUntil(() => !u.skill.active, 10);
|
||||
h.step();
|
||||
assert.equal(front.s.blockCnt, base, `${label(f)}: back after`);
|
||||
assert.equal(!!u.s.flags.camou, false, `${label(f)}: no 迷彩 after`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
+371
-99
@@ -3993,126 +3993,139 @@
|
||||
"snaps": "48868d9e c0fba9dd f1ed5a7f 45492016 1d7b7193 c7926ddf 2d138777 c3b2ae7b 63b5a8a6 be0fb4e1 3d3b899f 6d87b249 6587575a a6d3b62f 38c282fd 25732b97 f11d26b4 dd995a3a f7c48f36 475f3497 0f0fa445 b5ff96e3 0660ba17"
|
||||
},
|
||||
"diy-094": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
|
||||
"fast": true,
|
||||
"about": "5_diy1_a=sleach/S1 5_diy2_a=sleach/S2 5_diy1_a=sleach/S3 5_diy2_b=sleach/S1 5_diy1_b=sleach/S2 5_diy2_b=sleach/S3 5_diy1_b=sleach/S1/002_sleach 5_diy2_b=sleach/S2/002_sleach 5_diy1_b=sleach/S3/002_sleach 5_diy2_b=sleach/S1/003_sleach 5_diy1_b=sleach/S2/003_sleach 5_diy2_b=sleach/S3/003_sleach",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m06",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 12,
|
||||
"seed": 3107422628,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 201.93,
|
||||
"ticks": 6058,
|
||||
"reason": "cleared",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 0,
|
||||
"total": 90,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":90,"perfect":false,"leaks":90,"leaksUncounted":0,"coins":0,"dmg":51407,"bossDmg":0,"heal":7484,"deaths":21,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":0,"total":90,"perfect":false,"leaks":90,"leaksUncounted":0,"coins":0,"dmg":166775,"bossDmg":0,"heal":9967,"deaths":17,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,97,0,28,4256,0,2,0],
|
||||
["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,66,0,13494,11270,0,28,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1271,1,115,0,2450,2542,0,27,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,116,0,4334,3020,0,34,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,67,0,4670,2266,0,10,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,85,0,6588,2720,0,12,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,68,0,0,2436,3670,10,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,63,0,0,3302,3814,7,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,113,0,5925,5124,0,15,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,107,0,13920,2262,0,30,1]
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,93,0,13930,2972,294,35,3],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1339,1,7,0,0,0,0,0,5],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1339,1,9,0,15085,0,0,0,6],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",238.13,1,105,0,903,4766,0,35,2],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",1668,1,109,0,1463,3098,1430,52,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1668,1,114,0,8502,5183,1847,49,2],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_sleach",1818,1,115,0,14897,3636,0,45,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_sleach",0,0,108,0,24546,5147,1511,77,2],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_sleach",1620.3,1,88,0,56592,5561,3545,120,5],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_sleach",0,0,94,0,4873,3636,0,29,3],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_sleach",1818,1,113,0,1593,3158,1340,58,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_sleach",0,0,78,0,24391,3636,0,41,2]
|
||||
],
|
||||
"enemies": {"enemy_1116_liprr_2":[27,0,27,13892,31976],"enemy_1119_vofsd":[39,0,39,7161,10901],"enemy_1121_lifbos_2":[24,0,24,18145,8530]},
|
||||
"hooks": [1,116,6058,223,223,206,206,349,21,111,5,4,0,11353,45,23,90,90,1,21,0,0,0],
|
||||
"events": {"atk":223,"deploy":26,"die":21,"dmg":206,"fx":14,"heal":17,"leak":90,"skill":9,"spawn":102,"status":206},
|
||||
"fx": {"liberate":14},
|
||||
"snaps": "952a1d80 2f0f0d5f f2bdbb89 e8db3c70 f3853d8b 8f4446bc f4d82c02 fa5e04c8 18b7bcb9 0445bb38 8ece1312 65a9927c 2c377492 580b26ff 88379641 2eb28b76 c4a3698e 12d59c0b a3cd2fde 0d47f642 897ee040"
|
||||
"enemies": {"enemy_1116_liprr_2":[27,0,27,11449,128156],"enemy_1119_vofsd":[39,0,38,19434,27023],"enemy_1121_lifbos_2":[24,0,24,9910,11597]},
|
||||
"hooks": [1,115,6900,613,613,695,695,2647,17,106,38,36,0,36447,9762,37,90,89,1,17,0,0,0],
|
||||
"events": {"atk":613,"deploy":25,"die":17,"dmg":695,"fx":85,"leak":89,"skill":74,"spawn":102,"status":630},
|
||||
"fx": {"aoe":15,"dp":37,"liberate":5,"zone":28},
|
||||
"snaps": "4f65c948 a7488529 1d9ddd3a a390cb77 0a947f76 1dc3db1c cbb131ce 994c4d9d e44f2b9f 52317ea2 773ca0b8 170984fb dc69a94f 18859be4 00bcc557 5b677f3d 89caf568 f25479ae 2080aec0 f76455c8 bea56260 7851d186 4ed027e9"
|
||||
},
|
||||
"diy-095": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
|
||||
"about": "6_diy1_a=sleach/S1 6_diy2_a=sleach/S2 6_diy1_a=sleach/S3 6_diy2_b=sleach/S1 6_diy1_b=sleach/S2 6_diy2_b=sleach/S3 6_diy1_b=sleach/S1/002_sleach 6_diy2_b=sleach/S2/002_sleach 6_diy1_b=sleach/S3/002_sleach 6_diy2_b=sleach/S1/003_sleach 6_diy1_b=sleach/S2/003_sleach 6_diy2_b=sleach/S3/003_sleach",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m07",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"round": 13,
|
||||
"seed": 2719610662,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 186.43,
|
||||
"ticks": 5593,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 214.37,
|
||||
"ticks": 6431,
|
||||
"reason": "cleared",
|
||||
"killed": 65,
|
||||
"total": 90,
|
||||
"killed": 45,
|
||||
"total": 156,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 215,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":65,"total":90,"perfect":false,"leaks":25,"leaksUncounted":0,"coins":0,"dmg":588639,"bossDmg":0,"heal":50045,"deaths":6,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":45,"total":156,"perfect":false,"leaks":111,"leaksUncounted":0,"coins":0,"dmg":158103,"bossDmg":0,"heal":8026,"deaths":20,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,90,0,2431,5208,0,22,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",3371,1,106,1,2780,3981,0,14,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1529,1,100,8,64368,3082,0,35,5],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1904,1,93,18,142873,10262,0,77,6],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,12,13,107750,26742,0,126,4],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",2976.6,1,101,7,108379,15127,0,73,3],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1272,1,4,3,23337,0,0,55,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,7,7,49714,0,0,92,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1372,1,9,3,31223,0,0,39,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,11,5,55784,0,0,47,2],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1224,1,13,0,0,0,0,0,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,5,0,0,1500,50045,61,2],
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,1339,1,185,3,4934,2678,0,29,1],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,132,3,14319,3276,598,54,2],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,119,9,16271,2678,0,45,2],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,120,5,9892,3336,0,46,2],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",1668,1,184,0,8888,3336,0,36,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",1595.84,1,180,4,16697,3408,0,43,2],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_sleach",0,0,138,0,16563,6445,2709,56,2],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_sleach",0,0,174,1,11221,3736,0,46,2],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_sleach",1868,1,178,10,16264,1868,0,59,4],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_sleach",1868,1,177,1,7819,3225,1357,57,4],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_sleach",1868,1,144,5,9142,1868,0,60,3],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_sleach",1868,1,117,4,26095,5230,3362,83,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_1229_darmy":[27,19,8,6602,76927],"enemy_1229_darmy_2":[51,40,11,25602,212189],"enemy_9007_acelem":[12,6,6,33699,299524]},
|
||||
"hooks": [1,109,5593,736,736,1336,1416,112,71,96,30,28,0,31536,0,53,90,25,1,71,0,7,0],
|
||||
"events": {"atk":782,"deploy":17,"die":71,"dmg":1416,"fx":177,"heal":110,"leak":25,"skill":58,"spawn":104,"status":61},
|
||||
"fx": {"aoe":37,"beam":13,"burst":7,"dp":37,"slash":80,"spGain":3},
|
||||
"snaps": "7f6efb1f 0110b674 b3f9e162 8f5e5c60 2064df74 137a8184 d8ad578a fb70ee65 5de6cc0b a36f5030 4dcf550b d894bc10 fa706850 14776874 96fd0f22 8d647370 4ce5dc0d 2bf4e8bc 378b3869"
|
||||
"enemies": {
|
||||
"enemy_1197_sfshu": [15,7,8,366,16800],
|
||||
"enemy_1197_sfshu_2": [39,14,25,1501,49156],
|
||||
"enemy_1198_msfshu": [21,9,12,0,30],
|
||||
"enemy_1198_msfshu_2": [42,14,28,0,65],
|
||||
"enemy_9010_acpupp": [39,1,38,39217,92052]
|
||||
},
|
||||
"hooks": [1,185,6431,763,763,796,779,1390,65,176,29,25,0,29499,4162,55,156,111,1,69,0,0,0],
|
||||
"events": {"atk":763,"deploy":27,"die":65,"dmg":779,"fx":101,"leak":111,"skill":54,"spawn":170,"status":265},
|
||||
"fx": {"aoe":12,"dp":34,"ember":4,"revive":3,"shieldBreak":3,"summon":21,"telegraph":4,"zone":20},
|
||||
"snaps": "9ab0f026 6e2404bb 82e309ea bd322e57 88d176da de473536 6db81985 19f56b81 1cf2cd96 7fd32e17 3c6c9ec6 212cebb4 c3a30087 8d564abf 593382de 45b19991 bc6c14c0 9e2aca3c 152124a7 fd5ecede 27b4af57 aad5f380"
|
||||
},
|
||||
"diy-096": {
|
||||
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
|
||||
"about": "5_diy1_a=closur/S1 5_diy2_a=closur/S2 5_diy1_a=closur/S3 5_diy2_b=closur/S1 5_diy1_b=closur/S2 5_diy2_b=closur/S3 5_diy1_b=closur/S1/002_closur 5_diy2_b=closur/S2/002_closur 5_diy1_b=closur/S3/002_closur 6_diy1_a=closur/S1 6_diy2_a=closur/S2 6_diy1_a=closur/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m01",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"round": 12,
|
||||
"seed": 65368379,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 226.63,
|
||||
"ticks": 6799,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 215.77,
|
||||
"ticks": 6473,
|
||||
"reason": "cleared",
|
||||
"killed": 47,
|
||||
"total": 150,
|
||||
"killed": 33,
|
||||
"total": 102,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 398,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":47,"total":150,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":0,"dmg":517440,"bossDmg":0,"heal":0,"deaths":28,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":33,"total":102,"perfect":false,"leaks":69,"leaksUncounted":0,"coins":0,"dmg":211351,"bossDmg":0,"heal":0,"deaths":27,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,135,0,5981,5208,0,58,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,9,2,21421,3431,0,83,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,109,2,11923,3730,0,42,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,149,10,107184,4748,0,73,1],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,168,4,24242,4587,0,21,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,148,7,117115,3808,0,26,4],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,182,1,15464,9232,0,73,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",0,0,181,3,25520,7240,0,80,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,162,3,19570,5276,0,40,2],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",153.42,1,176,7,51410,6909,0,58,2],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,2,4,42304,0,0,129,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,3,4,75304,0,0,146,3]
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,1444,1,3,2,17550,0,0,164,10],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1444,1,4,3,23336,0,0,175,3],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1444,1,5,15,80835,0,0,238,3],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,144,0,1126,4965,0,29,2],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,143,1,6711,4965,0,35,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,139,0,7128,4965,0,31,1],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_closur",0,0,147,1,2345,5415,0,24,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_closur",0,0,141,4,15334,5415,0,39,1],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_closur",0,0,145,5,43636,5415,0,62,1],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,133,1,5361,4332,0,29,1],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,149,0,1664,4332,0,31,1],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,148,1,6326,4332,0,55,1],
|
||||
["p1","~1","token","token_10066_closur_ourbase",null,null,0,0,150,0,0,7860,0,0,0],
|
||||
["p1","~2","token","token_10066_closur_ourbase",null,null,2332.5,1,140,0,0,10767,0,0,0],
|
||||
["p1","~3","token","token_10066_closur_ourbase",null,null,2429.49,0,11,0,0,625,0,0,0],
|
||||
["p1","~4","token","token_10066_closur_ourbase",null,null,889.61,0,77,0,0,4350,0,0,0],
|
||||
["p1","~5","token","token_10066_closur_ourbase",null,null,2362.78,0,96,0,0,691,0,0,0],
|
||||
["p1","~6","token","token_10066_closur_ourbase",null,null,2588.06,0,134,0,0,32,0,0,0],
|
||||
["p1","~7","token","token_10066_closur_ourbase",null,null,3054,0,146,0,0,0,0,0,0]
|
||||
],
|
||||
"enemies": {"enemy_1249_lysdb_2":[60,20,40,16145,279875],"enemy_1251_lysyta":[51,23,28,2827,117497],"enemy_1329_cbshld":[39,4,35,35197,120068]},
|
||||
"hooks": [1,182,6799,1215,1215,1759,1784,0,75,178,18,30,0,23883,11457,79,150,103,1,76,0,0,18],
|
||||
"events": {"atk":1266,"deploy":32,"die":75,"dmg":1739,"fx":205,"leak":103,"skill":63,"spawn":162,"status":1070},
|
||||
"fx": {"aoe":42,"displace":16,"dodge":38,"dp":16,"shieldBreak":56,"slash":37},
|
||||
"snaps": "eedb1978 c0335295 4783edee f3486b2e 5f85509c 6d3f0014 b7df554c be565b07 bf617b2a 914ca1d1 507a8687 fbc7a618 a6d2c90e 3e84d000 c7cafa9a 3e8b4d04 ccd4d028 1b5edcab 2eb8a107 318e94df 71cd27b8 3df7cd04 4e95efb5"
|
||||
"enemies": {"enemy_1165_duhond":[63,33,30,8340,149464],"enemy_1174_duholy_2":[15,0,15,9879,23971],"enemy_1175_dushdo_2":[24,0,24,50243,37917]},
|
||||
"hooks": [1,146,6473,1233,1233,1728,1728,0,68,142,27,39,0,21120,0,72,102,69,1,68,0,0,0],
|
||||
"events": {"atk":1734,"deploy":44,"die":73,"dmg":1728,"fx":54,"leak":69,"skill":81,"spawn":121,"status":208},
|
||||
"fx": {"disappear":8,"dp":32,"summon":14},
|
||||
"snaps": "adeb9475 8cde7ee7 c44273a7 7366543a 71a25809 18290070 6376b468 e56825c8 41cf0c93 a6ea1e48 5283dae2 93e68357 e62b6a6e 6056011c 7b0f92f1 fba9d9a0 3f0c0819 a1c79203 ec090c31 ce937d08 625572ba 4e09675a"
|
||||
},
|
||||
"diy-097": {
|
||||
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
|
||||
"about": "6_diy1_b=closur/S1 6_diy2_b=closur/S2 6_diy1_b=closur/S3 6_diy2_b=closur/S1/002_closur 6_diy1_b=closur/S2/002_closur 6_diy2_b=closur/S3/002_closur 5_diy1_a=gdglow/S1 5_diy2_a=gdglow/S2 5_diy1_a=gdglow/S3 5_diy2_b=gdglow/S1 5_diy1_b=gdglow/S2 5_diy2_b=gdglow/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m02",
|
||||
@@ -4120,37 +4133,296 @@
|
||||
"round": 13,
|
||||
"seed": 3733794685,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 6,
|
||||
"time": 222,
|
||||
"ticks": 6660,
|
||||
"reason": "cleared",
|
||||
"killed": 4,
|
||||
"total": 105,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 913,
|
||||
"rngDraws": 202,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":6,"total":105,"perfect":false,"leaks":99,"leaksUncounted":0,"coins":0,"dmg":937154,"bossDmg":0,"heal":61210,"deaths":15,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":4,"total":105,"perfect":false,"leaks":101,"leaksUncounted":0,"coins":0,"dmg":598609,"bossDmg":0,"heal":0,"deaths":14,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,131,1,17294,5595,0,32,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,130,2,52003,7482,0,39,2],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,132,0,19347,18200,0,119,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1006.92,1,89,0,22744,51896,0,215,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,8,0,18480,0,0,136,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,9,1,47557,0,0,150,2],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,1372,1,13,0,104033,0,0,63,2],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,1,594540,0,0,92,3],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,28988,64,3],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,10,0,0,0,32222,49,2],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,1160,1,14,1,54357,0,0,126,2],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,121,0,6799,2900,0,13,0],
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"none",1655,1,8,0,8375,0,0,162,12],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"none",1655,1,9,0,17668,0,0,176,3],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"none",1655,1,16,0,20100,0,0,150,4],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_closur",1835,1,4,1,33060,0,0,204,12],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_closur",1835,1,6,0,78577,0,0,204,3],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_closur",1835,1,10,3,254255,0,0,278,4],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,1124,1,17,0,124536,0,0,82,3],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,132,0,1696,3372,0,14,0],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,129,0,1918,3372,0,12,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,144,0,14573,4080,0,10,1],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",1360,1,151,0,3842,2720,0,29,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,149,0,40007,4080,0,18,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_10066_closur_ourbase",null,null,3054,1,154,0,0,18324,0,0,0],
|
||||
["p1","~2","token","token_10066_closur_ourbase",null,null,0,0,152,0,0,21378,0,0,0],
|
||||
["p1","~3","token","token_10066_closur_ourbase",null,null,3054,1,153,0,0,21378,0,0,0]
|
||||
],
|
||||
"enemies": {"enemy_1116_liprr_2":[27,4,23,6683,217791],"enemy_1119_vofsd":[48,0,48,32538,175639],"enemy_1121_lifbos_2":[30,0,30,39482,205179]},
|
||||
"hooks": [1,151,6660,1512,1512,2370,2370,6793,38,139,43,59,0,33344,50,66,105,101,1,38,0,0,0],
|
||||
"events": {"atk":2064,"deploy":44,"die":38,"dmg":2358,"fx":393,"leak":101,"skill":126,"spawn":122,"status":209},
|
||||
"fx": {"disappear":20,"dp":40,"drone":202,"droneLock":34,"drones":5,"explode":22,"liberate":29,"shield":19,"summon":22},
|
||||
"snaps": "aefc11f0 65f0e3bc 55f21515 f77681c5 4da7991f 73a2a33f 93a7506d c5697857 fe16b7b2 881d16d1 b4deb0ba 88cfc955 cd4aea3e 55cc3ba2 65768d8a c6afb13f 461ce8a9 64138ccb f0ab5b31 91e8e0b0 da2eaee4 10935d20 e51cbfd2"
|
||||
},
|
||||
"diy-098": {
|
||||
"about": "5_diy1_b=gdglow/S1/002_gdglow 5_diy2_b=gdglow/S2/002_gdglow 5_diy1_b=gdglow/S3/002_gdglow 5_diy2_b=gdglow/S1/003_gdglow 5_diy1_b=gdglow/S2/003_gdglow 5_diy2_b=gdglow/S3/003_gdglow 6_diy1_a=gdglow/S1 6_diy2_a=gdglow/S2 6_diy1_a=gdglow/S3 6_diy2_b=gdglow/S1 6_diy1_b=gdglow/S2 6_diy2_b=gdglow/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m03",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 13,
|
||||
"seed": 1079403473,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 229.47,
|
||||
"ticks": 6884,
|
||||
"reason": "cleared",
|
||||
"killed": 1,
|
||||
"total": 66,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 159,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":1,"total":66,"perfect":false,"leaks":65,"leaksUncounted":0,"coins":0,"dmg":59983,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_gdglow",0,0,90,0,4888,4350,0,3,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_gdglow",828.56,1,91,0,1389,3521,0,11,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_gdglow",0,0,85,0,1507,4350,0,10,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_gdglow",0,0,47,0,892,2720,0,6,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_gdglow",0,0,76,0,384,2720,0,6,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_gdglow",0,0,9,0,662,1360,0,5,1],
|
||||
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,5,0,0,1124,0,0,0],
|
||||
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,7,0,632,1124,0,7,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,1124,1,10,0,40185,0,0,51,3],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,55,1,1681,2720,0,11,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,50,0,1570,2720,0,9,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,6,0,6193,1360,0,2,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[12,1,11,4030,13179],"enemy_1041_lazerd_2":[24,0,24,24040,27900],"enemy_1042_frostd":[30,0,30,0,18904]},
|
||||
"hooks": [1,91,6884,192,192,367,367,0,21,86,7,7,0,12606,175,0,66,65,1,21,0,0,0],
|
||||
"events": {"atk":192,"deploy":22,"die":21,"dmg":367,"fx":300,"leak":65,"skill":14,"spawn":81,"status":167},
|
||||
"fx": {"drone":159,"droneLock":120,"drones":7,"explode":14},
|
||||
"snaps": "ab6ec9e5 0cd29374 535b8f29 3a00102a 4e2f0e45 89bec34e 3229b975 60a9b6e7 f72a6124 fdcf62df d8ebef5d 58a1a24d 40484f5d 030d318d 2d3105b6 b2e4e0b0 fa368d3b a4d01055 23684e50 b6c74b76 de80150b 2b817302 aae3bd11"
|
||||
},
|
||||
"diy-099": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=gdglow/S1/002_gdglow 6_diy2_b=gdglow/S2/002_gdglow 6_diy1_b=gdglow/S3/002_gdglow 6_diy2_b=gdglow/S1/003_gdglow 6_diy1_b=gdglow/S2/003_gdglow 6_diy2_b=gdglow/S3/003_gdglow 5_diy1_a=csnipe 5_diy2_b=csnipe",
|
||||
"fast": true,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m04",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"seed": 213318073,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 228.97,
|
||||
"ticks": 6869,
|
||||
"reason": "cleared",
|
||||
"killed": 44,
|
||||
"total": 129,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 1205,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":44,"total":129,"perfect":false,"leaks":85,"leaksUncounted":0,"coins":0,"dmg":1076426,"bossDmg":0,"heal":0,"deaths":11,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,141,0,442,6384,0,32,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,128,0,2841,5816,0,77,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,150,2,6181,7692,0,24,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,149,1,3684,10002,0,31,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_gdglow",1485,1,3,14,219946,0,0,87,4],
|
||||
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_gdglow",1485,1,4,13,204111,0,0,41,1],
|
||||
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_gdglow",1485,1,11,3,168014,0,0,88,3],
|
||||
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_gdglow",1360,1,13,0,29119,0,0,48,3],
|
||||
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_gdglow",1360,1,5,5,225706,0,0,45,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_gdglow",1360,1,10,6,159660,0,0,96,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,12,0,24093,0,0,193,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,14,0,32630,0,0,190,3],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1116_liprr_2":[27,6,21,9816,240987],"enemy_1119_vofsd":[48,0,47,68191,434386],"enemy_1121_lifbos_2":[30,0,30,8066,261781]},
|
||||
"hooks": [1,132,6900,1215,1215,2060,2108,5872,21,119,19,24,0,34271,3565,27,105,98,1,21,0,0,0],
|
||||
"events": {"atk":1373,"deploy":25,"die":21,"dmg":2081,"fx":42,"heal":131,"leak":98,"skill":54,"spawn":119,"status":633},
|
||||
"fx": {"aoe":20,"displace":3,"dodge":4,"liberate":7,"spGain":8},
|
||||
"snaps": "1f17f098 a46fd1ea 4a4da16f 00118c9e 301b8f94 43fb2ce4 30085383 18a41889 a4cf28a0 651906c1 c289a97d d4e7ba5b bfeaf6ae 216c97d4 6ca7b7e7 4f28458f 599bf30a 0e3da688 e641a055 d5fd320a 5891c36b faf831cf ddfb2668"
|
||||
"enemies": {"enemy_10124_uashld_2":[18,1,17,7878,352011],"enemy_1043_zomsbr":[66,39,27,1291,322100],"enemy_1061_zomshd":[45,4,41,20725,439034]},
|
||||
"hooks": [1,150,6869,1151,1151,3763,3763,53819,55,140,25,23,0,35062,581,35,129,85,1,55,0,0,0],
|
||||
"events": {"atk":1151,"deploy":19,"die":55,"dmg":3763,"fx":1605,"leak":85,"skill":48,"spawn":143,"status":370},
|
||||
"fx": {"drone":1205,"droneLock":269,"drones":15,"explode":116},
|
||||
"snaps": "9996902b 61b66e98 732c6d67 48ae38a8 b8731a98 00463d97 bbba43ed 58597bf1 871b7fd8 4e975154 4ff72676 f097682e 29ef1afa 5fdb0354 b7df1987 d1307d71 a8af977d 090fb008 25361ebd ff7fb0e8 ca2e7947 559c1e70 cec6ba4d"
|
||||
},
|
||||
"diy-100": {
|
||||
"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_abyss",
|
||||
"round": 12,
|
||||
"seed": 1853641125,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 203.93,
|
||||
"ticks": 6118,
|
||||
"reason": "cleared",
|
||||
"killed": 60,
|
||||
"total": 153,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 131,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":60,"total":153,"perfect":false,"leaks":93,"leaksUncounted":0,"coins":0,"dmg":727055,"bossDmg":0,"heal":52388,"deaths":10,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,114,0,9773,5208,0,45,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,129,1,38916,6742,0,80,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,162,8,21954,8669,0,25,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1904,1,169,29,281570,24533,0,100,6],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,15,7,102220,25677,0,137,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,136,4,66511,6822,0,38,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,4,40791,0,0,108,3],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,5,6,63600,0,0,148,3],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,11,0,0,0,25465,59,3],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,13,0,0,0,26923,57,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,6,1,39821,0,0,112,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,14,0,61898,0,0,137,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_1195_sfyin": [45,11,34,18165,212500],
|
||||
"enemy_1196_msfyin": [22,20,2,0,41],
|
||||
"enemy_1197_sfshu": [39,11,28,33792,288323],
|
||||
"enemy_1198_msfshu": [33,16,17,0,58],
|
||||
"enemy_1199_sfjin": [12,2,10,25694,226118],
|
||||
"enemy_1200_msfjin": [2,0,2,0,15]
|
||||
},
|
||||
"hooks": [1,174,6118,1612,1612,2011,1965,116,70,163,32,32,0,34916,243,55,153,93,1,79,0,0,46],
|
||||
"events": {"atk":1612,"deploy":18,"die":70,"dmg":1965,"fx":206,"heal":116,"leak":93,"skill":64,"spawn":168,"status":496},
|
||||
"fx": {"aoe":36,"dodge":46,"dp":44,"slash":56,"summon":24},
|
||||
"snaps": "e63c08bc 993deae4 dd02af70 2d6546d3 04bbd4f3 1830ce73 2a424989 7fa082c5 ce8dd734 cc64a140 25615b6b 12274f2a dd8c1029 a4f58d5c 46f041f6 104cd848 39c8bf6f abe3608e 30a3b7d5 ab980aa3 d803a54b"
|
||||
},
|
||||
"diy-101": {
|
||||
"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_funny",
|
||||
"round": 12,
|
||||
"seed": 739544218,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 204.8,
|
||||
"ticks": 6144,
|
||||
"reason": "cleared",
|
||||
"killed": 87,
|
||||
"total": 99,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 1064,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":87,"total":99,"perfect":false,"leaks":12,"leaksUncounted":0,"coins":0,"dmg":704152,"bossDmg":0,"heal":47857,"deaths":1,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,841,1,3,20,91078,1024,0,124,3],
|
||||
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,1,6,68740,0,0,55,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,311,1,4,0,1569,843,0,26,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,75,1,7264,1902,0,29,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,5,16,84001,0,0,202,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,8,18,178620,0,0,188,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,6,0,18491,0,0,17,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,9,20,187306,0,0,77,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,10,0,0,8016,24220,63,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,11,0,0,14535,23637,67,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,12,5,36418,24853,0,129,4],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,2,1,30664,0,0,54,2]
|
||||
],
|
||||
"enemies": {"enemy_1010_demon":[42,41,1,41095,296286],"enemy_1320_wdrrl_2":[12,2,10,0,174829],"enemy_1422_lrsldr_2":[45,44,1,10077,233038]},
|
||||
"hooks": [1,112,6144,1243,1243,1798,1861,200,88,100,24,22,0,36141,6922,29,99,12,1,88,0,0,0],
|
||||
"events": {"atk":1498,"deploy":13,"die":88,"dmg":1860,"fx":40,"heal":155,"leak":12,"skill":51,"spawn":111,"status":674},
|
||||
"fx": {"aoe":13,"displace":1,"dodge":26},
|
||||
"snaps": "9b12b5a2 cc58812c b15c7a02 65275a10 32f7558d 695f18fe 4167d093 a33b1752 1bd38a64 97735c90 9cfcd534 393bf4da dc5c2c12 2b2038b4 dbf5688c 52665c6d 997dacea 92e59d36 d02c5b07 0d9e13e2 efc5468b"
|
||||
},
|
||||
"diy-102": {
|
||||
"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_hard",
|
||||
"round": 13,
|
||||
"seed": 2379881591,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 201.57,
|
||||
"ticks": 6047,
|
||||
"reason": "cleared",
|
||||
"killed": 261,
|
||||
"total": 297,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 207,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":261,"total":297,"perfect":false,"leaks":36,"leaksUncounted":0,"coins":0,"dmg":479266,"bossDmg":0,"heal":53831,"deaths":10,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,2604,1,253,12,2143,2604,0,41,2],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",2890.55,1,7,4,24083,33048,0,133,3],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1207.76,1,268,37,35114,4792,0,39,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,129,84,102697,5299,0,57,6],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,2343.99,1,288,5,10524,7451,0,22,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",2020.71,1,132,20,93856,20393,0,81,3],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,5,3227,0,0,28,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,12,17,31071,0,0,65,2],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,14,1,17185,0,0,23,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,76,159366,0,0,73,2],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,40735,67,2],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,152,0,0,4115,13096,22,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_1195_sfyin": [39,31,8,2507,109166],
|
||||
"enemy_1196_msfyin": [62,60,2,0,122],
|
||||
"enemy_1197_sfshu": [48,42,6,5882,183185],
|
||||
"enemy_1198_msfshu": [126,118,8,0,366],
|
||||
"enemy_1207_sfji": [12,10,2,69314,186308],
|
||||
"enemy_1208_msfji": [10,0,10,0,119]
|
||||
},
|
||||
"hooks": [1,320,6047,960,960,1820,1800,140,271,307,28,25,0,31391,0,80,297,36,1,272,0,0,20],
|
||||
"events": {"atk":1011,"deploy":20,"die":271,"dmg":1800,"fx":414,"heal":133,"leak":36,"skill":53,"spawn":312,"status":40},
|
||||
"fx": {"aoe":97,"dodge":20,"dp":130,"slash":72,"spGain":12,"summon":83},
|
||||
"snaps": "6506a5c4 821dac7d 9216e8f9 291487fb 5d47614f 5dff6977 3eeeee66 9a21bbca 8e530e36 4e93e9d1 54f80175 b1404805 b528127f 7fbbfb92 0cf1a8e3 356ad797 4d9a6a79 edae73d6 39b44426 08667abf 724dcbc6"
|
||||
},
|
||||
"diy-103": {
|
||||
"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_normal",
|
||||
"round": 13,
|
||||
"seed": 1753717681,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 210.67,
|
||||
"ticks": 6320,
|
||||
"reason": "cleared",
|
||||
"killed": 23,
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 202,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":23,"total":123,"perfect":false,"leaks":100,"leaksUncounted":0,"coins":0,"dmg":407198,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,86,2,27050,3730,0,26,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,126,14,178020,4988,0,57,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,148,0,42484,9232,0,50,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",651.83,1,150,3,84913,11220,0,77,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,160,1,132,0,30869,2160,0,81,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",450,1,133,2,43862,2450,0,84,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_1148_dssbr":[51,12,39,8112,169910],"enemy_1148_dssbr_2":[60,11,49,15328,229068],"enemy_1439_dslntf":[12,0,12,10340,113857]},
|
||||
"hooks": [1,150,6320,499,499,2118,2622,0,44,144,6,19,0,7621,4142,38,123,100,1,44,0,10,0],
|
||||
"events": {"atk":538,"deploy":24,"die":44,"dmg":2564,"fx":61,"leak":100,"skill":42,"spawn":132,"status":458},
|
||||
"fx": {"burst":10,"displace":45,"dodge":6},
|
||||
"snaps": "76a5789a 0d8fe1fc aca88ba3 93412a3b 57642d38 7d0e9d04 05edb1c1 12d5b5b4 b880f5a2 706856a1 61435e3d a65a20a2 35fd9300 9021e870 94a24b43 85a49207 c097c8a1 9acd4c1a a1420866 0769fc29 60f3e7a3"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user