feedback5 O18: 自选 kits for 阿, 傀影 (with his 镜中虚影) and 弑君者
Three owned-6★ 自选 picks get operator kits (kits/ops/op-haak.js, op-phatom.js, op-crosly.js; appended to OPERATOR_KIT_FILES): every skill, both talents, the trait and every module at stage 1 / 3 in every DIY form, from the data, PRTS (备注) and the client's battle logic (charpacks, skill / equip prefabs, buff templates). - 阿: 怪杰 drain (profession default), 混合药物射击 (one of four per attack, GEE-X stage 2+ "all four" roll), 药剂扩散 (healing received, GEE-Y stage 3 deploy heal on the ally ahead), S1 ASPD, S2 / S3 爆发剂 (15 hits of 500 fixed ATK on the ally of his range ahead, then the buff on both), GEE-X SP recovery while HP >= 80 %. - 傀影: S1 dodge + physical-only barrier, S2 stacked ATK used per damage instance, S3 burst (x-4, ground, radial 小力 push, one random status per cast); the twin's kit (its skill copy, killed when he leaves, back on its tile once its own redeploy time has run from its last deployment and he stands, 5 DP), 虚影精通, EXE-X (refund N/A), EXE-Y (lonely ATK, stage 3 ATK on both), ISW-A attributes only. - 弑君者: the smoke's 命中率 cut (x-4, S3 x-1, S2 x2; battle-wide hit handler), 弑君者威名 (fresh ground enemies x1.2, EXE-Y stage 3 x1.35 and stun x1.5), S1 ATK / dodge, S2 no attacks + taunt -1 + end blast, S3 隐匿 + block 0 + 2 s strikes with 6 s marks. Tests: test/content/op_haak.test.js (10), op_phatom.test.js (8), op_crosly.test.js (8). Golden: only diy moved — diy-094..diy-100 field the 78 new configurations (阿 24, 傀影 30, 弑君者 24; no summon is placed by the family), diy-101..diy-104 are the 预备干员 / prototype battles re-packed after them (new index, stage and seed). roster, bonds, fields, matches and standins are unchanged.
This commit is contained in:
1 parent
78251f499c
commit
b55d4f9d04
8 files changed
+1909
-99
No files matched your search
@@ -0,0 +1,302 @@
|
||||
// test/content/op_crosly.test.js — the 自选 operator kit of 弑君者 (char_1502_crosly, 6★ 处决者; kit
|
||||
// server/sim/content/kits/ops/op-crosly.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, EXE-X 弑君刃
|
||||
// or EXE-Y 木炭画 at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json (the form of that slot
|
||||
// status); the fidelity checklist of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_crosly.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const CROSLY = 'char_1502_crosly';
|
||||
const FORMS = BACKUPS.units[CROSLY].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const EX = 'uniequip_002_crosly', EY = 'uniequip_003_crosly';
|
||||
const S1 = 'skchr_crosly_1', S2 = 'skchr_crosly_2', S3 = 'skchr_crosly_3';
|
||||
const YAK = 'chess_char_1_02_a', TEXAS = 'chess_char_1_08_a';
|
||||
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 a form fights with: the module's changes composed (index 0 / 1, −1 = hidden). */
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const base = formOf(tier, elite).talents.find((t) => t.index === i)?.bb ?? {};
|
||||
const ch = (elite ? modOf(tier, mod)?.talentChanges ?? [] : []).find((t) => t.talentIndex === i)?.bb ?? {};
|
||||
return { ...base, ...ch };
|
||||
};
|
||||
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_hitter: dummy('enemy_hitter', { atk: 200, range: 1.5, bat: 0.5 }), enemy_caster: dummy('enemy_caster', { atk: 200, range: 1.5, bat: 0.5, dmgType: 'arts' }),
|
||||
enemy_flyhitter: dummy('enemy_flyhitter', { atk: 200, range: 1.5, bat: 0.5, motion: 'FLY' }),
|
||||
};
|
||||
/** Every 自选 form: [tier, elite, module]. */
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, EX, EY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** A battle with 弑君者 as uid 1 at (row, col) facing RIGHT, plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 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', 'death'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: CROSLY, 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]));
|
||||
}
|
||||
/** Knock `u` out and bring it straight back on its tile (free). */
|
||||
function redeployNow(h, u) {
|
||||
h.b.kill(u, null);
|
||||
assert.ok(h.b.redeploy(u, { free: true }), `${u.name} redeployed`);
|
||||
}
|
||||
/**
|
||||
* Watch the 命中率 cut `hitter` suffers from now on: its attacks counted before (priority 295) and after (270) the smoke's
|
||||
* handler (280); allies are kept alive. Returns () => { n, rate }.
|
||||
*/
|
||||
function watch(h, hitter) {
|
||||
const seen = new Set(), passed = new Set();
|
||||
h.b.on('hit', (c) => { if (c.source === hitter && c.dmg.isAttack) seen.add(c.dmg.attackId); }, { priority: 295 });
|
||||
h.b.on('hit', (c) => { if (c.source === hitter && c.dmg.isAttack && !c.dmg.cancel) passed.add(c.dmg.attackId); }, { priority: 270 });
|
||||
h.b.on('fatal', (c) => { if (c.unit.side === 'ally') c.prevented = true; });
|
||||
return () => ({ n: seen.size, rate: seen.size ? 1 - passed.size / seen.size : 0 });
|
||||
}
|
||||
|
||||
test('弑君者 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, melee ground-only, 22 s redeploy, 叙拉古, no 特质; each skill a timed deployment skill (被动 ON_DEPLOY), running from the deployment', () => {
|
||||
assert.equal(OPERATOR_KITS[CROSLY], KITS[CROSLY]);
|
||||
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, rec = form.skills[skill];
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [CROSLY, SLOT[tier], rec.skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.respawnTime, u.base.cost],
|
||||
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 22, 12], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.profile.attack, u.profile.dmgType, u.profile.canHitFly], ['melee', 'phys', false], `${label(f)}: 处决者`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1 (S3 too)`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['siracusaShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.deepEqual([rec.skillType, rec.spType, u.skill.kind, u.skill.rule, u.skill.active, u.skill.activations], ['PASSIVE', 'ON_DEPLOY', 'duration', 'NEVER', true, 1], `${label(f)}: S${skill + 1} from the deployment`);
|
||||
approx(u.skill.timeLeft, [rec.duration, rec.bb.duration, rec.duration][skill], `${label(f)}: S${skill + 1} duration`, 0.01);
|
||||
assert.equal(u.s.blockCnt, skill === 2 ? 0 : 1, `${label(f)}: blocks ${skill === 2 ? 0 : 1}`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the numbers (zh_CN): E2 Lv1 1322 / 425 / 273, E2 Lv60 1569 / 505 / 307; EXE-X +90 / +34 / +23 → +130 / +45 / +30,
|
||||
// EXE-Y +84 / +63 → +135 / +88 HP / ATK
|
||||
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1322, 425, 1569, 505]);
|
||||
assert.deepEqual([modOf(5, EX).attr, modOf(6, EX).attr, modOf(5, EY).attr, modOf(6, EY).attr],
|
||||
[{ maxHp: 90, atk: 34, def: 23 }, { maxHp: 130, atk: 45, def: 30 }, { maxHp: 84, atk: 63 }, { maxHp: 135, atk: 88 }]);
|
||||
});
|
||||
|
||||
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(CROSLY));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(CROSLY), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: CROSLY, skillIndex: 2, uniEquipId: EY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[5]]: { charId: CROSLY, skillIndex: 2, uniEquipId: EY } } });
|
||||
});
|
||||
|
||||
test('S1 尘烟蔽目 (被动): for 10 s from each deployment ATK +65 % / +80 % and 30 % / 40 % physical and arts dodge', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EX]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.prob], [10, elite ? 0.8 : 0.65, elite ? 0.4 : 0.3]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), 'ATK');
|
||||
assert.deepEqual([u.s.dodgePhys, u.s.dodgeArts], [sk.bb.prob, sk.bb.prob], 'dodge');
|
||||
h.run(10.1);
|
||||
assert.deepEqual([u.skill.active, u.s.atk, u.s.dodgePhys, u.s.dodgeArts], [false, u.base.atk, 0, 0], 'over');
|
||||
redeployNow(h, u);
|
||||
assert.ok(u.skill.active && u.skill.activations === 2, 'again at a redeployment');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 硝烟震爆 (被动): for 8 s no attacks and taunt level −1; then 270 % / 350 % ATK physical to every ground enemy of the x-4 smoke (fresh ones ×T2); no blast when she is stunned as it ends', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EY]]) {
|
||||
const sk = skillOf(tier, elite, S2), t1 = talentOf(tier, elite, mod, 1);
|
||||
assert.deepEqual([sk.bb.duration, sk.bb.taunt_level, sk.bb['attack@atk_scale_s2'], sk.bb.talent_scale], [8, -1, elite ? 3.5 : 2.7, 2]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||||
assert.equal(u.s.taunt, -1, 'taunt level −1');
|
||||
const front = h.spawn('enemy_dummy', { pos: [10, 5] }), diag = h.spawn('enemy_dummy', { pos: [11, 3] });
|
||||
const marked = h.spawn('enemy_dummy', { pos: [9, 4] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 5] }), far = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.step();
|
||||
h.b.dealDamage(marked, u, { amount: 1, type: 'phys', canDodge: false }); // it has damaged her
|
||||
h.run(7);
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u).length, 0, 'no attacks while it runs');
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 2), 'over after 8 s');
|
||||
const blast = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.tags?.includes('crosly:blast'));
|
||||
assert.deepEqual(blast.map((c) => c.target).sort((a, b) => a.id - b.id), [front, diag, marked].sort((a, b) => a.id - b.id), 'the ground enemies of her x-4');
|
||||
for (const c of blast) {
|
||||
approx(c.amount, u.s.atk * sk.bb['attack@atk_scale_s2'] * (c.target === marked ? 1 : t1.damage_scale), `${c.target === marked ? 'marked' : 'fresh'}: ${sk.bb['attack@atk_scale_s2'] * 100} %`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true]);
|
||||
}
|
||||
assert.ok(!blast.some((c) => c.target === fly || c.target === far), 'flyer / outside untouched');
|
||||
assert.equal(u.s.taunt, 0, 'taunt back');
|
||||
// stunned as it ends: no blast
|
||||
redeployNow(h, u);
|
||||
h.run(7.5);
|
||||
h.b.applyStatus(u, 'stun', { duration: 2, force: true });
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
h.run(1);
|
||||
assert.ok(!u.skill.active, 'ended');
|
||||
assert.equal(h.hooksOf('damaged').slice(n0).filter((c) => c.dmg?.tags?.includes('crosly:blast')).length, 0, 'stunned: no blast');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 烽烟行刑场 (被动, 16 s): 隐匿 and block count 0 (enemies neither target nor are blocked by her); a strike every 2 s on one unmarked ground enemy of the x-1 — 2 hits × 150 % / 190 % ATK and 晕眩 1.5 s — marking it 6 s; her range stays 1-1; all back after', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EX]]) {
|
||||
const sk = skillOf(tier, elite, S3), t1 = talentOf(tier, elite, mod, 1);
|
||||
assert.deepEqual([sk.duration, sk.rangeId, sk.bb['attack@s3_cd'], sk.bb['attack@times'], sk.bb['attack@atk_scale_s3'], sk.bb['attack@stun'], sk.bb.mark_duration],
|
||||
[16, 'x-1', 2, 2, elite ? 1.9 : 1.5, 1.5, 6]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
assert.deepEqual([!!u.s.flags.stealth, u.s.blockCnt, u.liveRangeGrid], [true, 0, formOf(tier, elite).rangeGrid], '隐匿, block 0, 1-1');
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 5] }), b = h.spawn('enemy_dummy', { pos: [12, 4] }), c = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 4] }), far = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
const hitter = h.spawn('enemy_hitter', { pos: [11, 4] });
|
||||
h.run(15.5);
|
||||
const hits = h.hooksOf('damaged').filter((x) => x.source === u && x.dmg?.isAttack);
|
||||
assert.equal(hits.length % 2, 0, 'two hits a strike');
|
||||
const strikes = [];
|
||||
for (const x of hits) if (!strikes.length || Math.abs(strikes[strikes.length - 1].t - x.t) > 1e-6) strikes.push({ t: x.t, target: x.target });
|
||||
assert.ok(strikes.length >= 7, `${strikes.length} strikes`);
|
||||
for (let i = 1; i < strikes.length; i++) approx(strikes[i].t - strikes[i - 1].t, 2, 'every 2 s', 0.02);
|
||||
assert.ok(hits.every((x) => [a, b, c, hitter].includes(x.target)), 'unmarked ground enemies of the x-1 only (no flyer, nothing 3 tiles away)');
|
||||
assert.ok(!hits.some((x) => x.target === fly || x.target === far));
|
||||
for (const x of hits) {
|
||||
assert.deepEqual([x.type, x.dmg.isSkill], ['phys', true]);
|
||||
approx(x.amount, u.s.atk * sk.bb['attack@atk_scale_s3'] * t1.damage_scale, `${sk.bb['attack@atk_scale_s3'] * 100} % ×T2 (fresh)`);
|
||||
}
|
||||
// the 6 s mark: never the same target twice within 6 s
|
||||
const last = new Map();
|
||||
for (const st of strikes) {
|
||||
if (last.has(st.target)) assert.ok(st.t - last.get(st.target) >= 6 - 1e-6, `marked 6 s (${st.target.defId})`);
|
||||
last.set(st.target, st.t);
|
||||
}
|
||||
const stuns = h.hooksOf('statusApplied').filter((x) => x.source === u && x.status === 'stun');
|
||||
assert.equal(stuns.length, strikes.length, 'one 晕眩 a strike');
|
||||
for (const x of stuns) approx(x.duration, sk.bb['attack@stun'], '1.5 s');
|
||||
assert.equal(h.hooksOf('damaged').filter((x) => x.source === hitter && x.target === u).length, 0, '隐匿: the hitter beside her never targets her');
|
||||
assert.equal(u.blocking.length, 0, 'blocks nothing');
|
||||
h.run(1);
|
||||
assert.deepEqual([u.skill.active, !!u.s.flags.stealth, u.s.blockCnt], [false, false, 1], 'all back after 16 s');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 吞咽苦厄: while her skill runs, the attacks of ground enemies in her smoke miss 20 % (EXE-X stage 3: 25 %; S2: ×2) — x-4, S3 x-1; none outside it, none for air units, none once the skill is over', () => {
|
||||
// S1, normal: a hitter in her x-4 (beside her) — 20 %; another two tiles ahead — 0 %
|
||||
{
|
||||
const t0 = talentOf(5, false, null, 0);
|
||||
assert.deepEqual([t0.damage_hitrate_physical, t0.damage_hitrate_magical], [-0.2, -0.2]);
|
||||
const { h, u } = field({ tier: 5, skill: 0, others: [{ uid: 2, chessId: YAK, row: 10, col: 7 }] });
|
||||
const inside = h.spawn('enemy_caster', { pos: [11, 4] }), outside = h.spawn('enemy_hitter', { pos: [10, 6] });
|
||||
u.skill.extend(400);
|
||||
const wIn = watch(h, inside), wOut = watch(h, outside);
|
||||
h.run(300);
|
||||
const rIn = wIn(), rOut = wOut();
|
||||
assert.ok(rIn.n > 300 && Math.abs(rIn.rate - 0.2) < 0.05, `arts attacker in the x-4: ${rIn.rate} of ${rIn.n}`);
|
||||
assert.ok(rOut.n > 300 && rOut.rate === 0, `two tiles ahead: ${rOut.rate} of ${rOut.n}`);
|
||||
u.skill.extend(-999);
|
||||
h.step(2);
|
||||
const wAfter = watch(h, inside);
|
||||
h.run(60);
|
||||
const after = wAfter();
|
||||
assert.ok(after.n > 60 && after.rate === 0, 'skill over: no smoke');
|
||||
done(h);
|
||||
}
|
||||
// S2 at EXE-X stage 3: 25 % × 2
|
||||
{
|
||||
const t0 = talentOf(6, true, EX, 0);
|
||||
assert.equal(t0.damage_hitrate_physical, -0.25);
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: EX, skill: 1 });
|
||||
const hitter = h.spawn('enemy_hitter', { pos: [10, 5] });
|
||||
const fly = h.spawn('enemy_flyhitter', { pos: [9, 4] });
|
||||
u.skill.extend(400);
|
||||
const w = watch(h, hitter), wFly = watch(h, fly);
|
||||
h.run(300);
|
||||
const r = w(), rFly = wFly();
|
||||
assert.ok(r.n > 300 && Math.abs(r.rate - 0.5) < 0.05, `S2 ×2: ${r.rate} of ${r.n}`);
|
||||
assert.ok(rFly.n > 300 && rFly.rate === 0, `an air unit in the smoke: ${rFly.rate} of ${rFly.n}`);
|
||||
done(h);
|
||||
}
|
||||
// S3: the x-1 reaches two tiles ahead
|
||||
{
|
||||
const { h, u } = field({ tier: 5, skill: 2, others: [{ uid: 2, chessId: YAK, row: 10, col: 7 }] });
|
||||
const hitter = h.spawn('enemy_hitter', { pos: [10, 6] });
|
||||
u.skill.extend(400);
|
||||
const w = watch(h, hitter);
|
||||
h.run(300);
|
||||
const r = w();
|
||||
assert.ok(r.n > 150 && Math.abs(r.rate - 0.2) < 0.06, `S3 x-1: ${r.rate} of ${r.n}`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 弑君者威名: physical damage she deals to a ground enemy that has not damaged her this deployment ×1.2 (EXE-Y stage 3: ×1.35, and her 晕眩 on it ×1.5); a 持续伤害 tick marks nothing; the marks go when she leaves the field', () => {
|
||||
for (const f of [[5, false, null], [6, true, EY], [5, true, EY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t1 = talentOf(tier, elite, mod, 1), hidden = talentOf(tier, elite, mod, -1);
|
||||
const y3 = elite && mod === EY && tier === 6;
|
||||
assert.deepEqual([t1.damage_scale, hidden.one_minus_status_resistance ?? 0], y3 ? [1.35, 0.5] : [1.2, 0], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
h.run(10.1); // S1 over: plain attacks
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
const hitsOn = () => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.dmg?.isAttack);
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk * t1.damage_scale, `${label(f)}: fresh ×${t1.damage_scale}`);
|
||||
h.b.dealDamage(e, u, { amount: 5, type: 'true', canDodge: false, tags: ['dot'] });
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk * t1.damage_scale, `${label(f)}: a DoT tick marks nothing`);
|
||||
h.b.dealDamage(e, u, { amount: 5, type: 'phys', canDodge: false });
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk, `${label(f)}: it damaged her ⇒ ×1`);
|
||||
// EXE-Y stage 3: a stun of hers lasts ×1.5 on a fresh enemy only
|
||||
const fresh = h.spawn('enemy_dummy', { pos: [9, 5] });
|
||||
h.b.applyStatus(fresh, 'stun', { duration: 2, source: u });
|
||||
h.b.applyStatus(e, 'stun', { duration: 2, source: u });
|
||||
const st = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun').slice(-2);
|
||||
approx(st[0].duration, y3 ? 3 : 2, `${label(f)}: fresh ×${y3 ? 1.5 : 1}`);
|
||||
approx(st[1].duration, 2, `${label(f)}: marked ×1`);
|
||||
// her redeployment clears the marks
|
||||
redeployNow(h, u);
|
||||
h.run(10.1);
|
||||
h.run(2);
|
||||
approx(hitsOn().slice(-1)[0].amount, u.s.atk * t1.damage_scale, `${label(f)}: fresh again after a redeployment`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('EXE-X 弑君刃: the refund has no effect (no retreat in battle); EXE-Y 木炭画: ATK +10 % while no allied operator stands on the 4 tiles beside her (stages 1 and 3)', () => {
|
||||
{
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: EX, skill: 0 });
|
||||
assert.deepEqual(u.def.traitBb, { withdraw_cost_recover_ratio: 0.8 });
|
||||
h.b.players[0].dp = 3;
|
||||
h.b.retreat(u);
|
||||
h.step();
|
||||
assert.equal(h.b.players[0].dp, 3, 'no refund');
|
||||
done(h);
|
||||
}
|
||||
for (const f of [[5, true, EY], [6, true, EY], [6, true, EX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, others: [{ uid: 2, chessId: TEXAS, row: 12, col: 7 }] });
|
||||
const texas = h.unit(2);
|
||||
assert.equal(!!u.findBuff('trait:crosly:lonely'), mod === EY, `${label(f)}: alone`);
|
||||
if (mod === EY) approx(u.s.atk, u.base.atk * 1.1, `${label(f)}: ATK +10 %`);
|
||||
assert.ok(h.b.relocate(texas, 10, 5));
|
||||
h.step();
|
||||
assert.equal(u.findBuff('trait:crosly:lonely'), null, `${label(f)}: an operator beside her`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
// test/content/op_haak.test.js — the 自选 operator kit of 阿 (char_225_haak, 6★ 怪杰; kit
|
||||
// server/sim/content/kits/ops/op-haak.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, GEE-X
|
||||
// 什锦果味医用箱 or GEE-Y 毒医残章 at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json (the
|
||||
// form of that slot status); the fidelity checklist of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_haak.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const HAAK = 'char_225_haak';
|
||||
const FORMS = BACKUPS.units[HAAK].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const GX = 'uniequip_002_haak', GY = 'uniequip_003_haak';
|
||||
const S1 = 'skchr_haak_1', S2 = 'skchr_haak_2', S3 = 'skchr_haak_3';
|
||||
const YAK = 'chess_char_1_02_a', TEXAS = 'chess_char_1_08_a', LISKAM = 'chess_char_1_20_a';
|
||||
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 a form fights with: the module's changes composed (index 0 / 1). */
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const base = formOf(tier, elite).talents.find((t) => t.index === i)?.bb ?? {};
|
||||
const ch = (elite ? modOf(tier, mod)?.talentChanges ?? [] : []).find((t) => t.talentIndex === i)?.bb ?? {};
|
||||
return { ...base, ...ch };
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }) };
|
||||
/** Every 自选 form: [tier, elite, module]. */
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, GX, GY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** A battle with 阿 as uid 1 at (row, col) facing RIGHT, plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 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', 'heal', 'spGain'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: HAAK, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Keep him from casting (no SP). */
|
||||
function noSkill(h, u) {
|
||||
u.skill.charges = 0; u.skill.sp = 0;
|
||||
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
|
||||
}
|
||||
|
||||
test('阿 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 3-3, blocks 1, ranged physical arrows that hit air units, the 怪杰 drain, 炎, no 特质, the data triggers', () => {
|
||||
assert.equal(OPERATOR_KITS[HAAK], KITS[HAAK]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null;
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [HAAK, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd, u.base.respawnTime],
|
||||
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, 100 + (m?.attr.aspd ?? 0), 70 + (m?.attr.respawnTime ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.profile.projectile, u.profile.hpDrain], [1, 'ranged', 'phys', true, 'arrow', 0.01], `${label(f)}: 怪杰`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-3`);
|
||||
assert.equal(form.rangeId, '3-3');
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['yanShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule, form.skills[skill].trigger.rule], ['duration', 'DEFAULT', 'DEFAULT'], `${label(f)}: S${skill + 1} trigger (data)`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the numbers of the forms (zh_CN): E2 Lv1 1627 / 583 / 121, E2 Lv60 1897 / 663 / 142; GEE-X +135 / +37 → +240 / +57 HP / ATK,
|
||||
// GEE-Y +43 / +3 → +75 / +5 ATK / ASPD and respawn −15
|
||||
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], [1627, 583, 1897, 663]);
|
||||
assert.deepEqual([modOf(5, GX).attr, modOf(6, GX).attr, modOf(5, GY).attr, modOf(6, GY).attr],
|
||||
[{ maxHp: 135, atk: 37 }, { maxHp: 240, atk: 57 }, { atk: 43, aspd: 3, respawnTime: -15 }, { atk: 75, aspd: 5, respawnTime: -15 }]);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 阿 is offered at tiers 5 and 6 (he has a kit) and a roster with him passes validateDiyPicks', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(HAAK));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(HAAK), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: HAAK, skillIndex: 2, uniEquipId: GY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: HAAK, skillIndex: 2, uniEquipId: GY } } });
|
||||
});
|
||||
|
||||
test('trait 怪杰: 1 % of his max HP lost every second (a 流失, never below 1 HP); he shoots an air unit in his range', () => {
|
||||
for (const f of [[5, false, null], [6, true, GX]]) {
|
||||
const { h, u } = field({ tier: f[0], elite: f[1], mod: f[2], skill: 0 });
|
||||
noSkill(h, u);
|
||||
assert.deepEqual(u.def.traitBb, { hp_ratio: 0.01 }, label(f));
|
||||
h.run(3.05);
|
||||
approx(u.hp, u.s.maxHp * (1 - 0.03), `${label(f)}: three ticks of 1 %`, 1e-6);
|
||||
u.hp = 5;
|
||||
h.run(10);
|
||||
assert.equal(u.hp, 1, `${label(f)}: not lethal — 1 HP kept`);
|
||||
assert.ok(u.alive);
|
||||
u.hp = u.s.maxHp;
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 6] });
|
||||
h.run(3);
|
||||
assert.ok(atkHits(h, u).some((c) => c.target === fly && c.type === 'phys'), `${label(f)}: physical arrows at the flyer`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('GEE-X “什锦果味医用箱” (stages 1 and 3): SP recovery +0.25/s while his HP ratio is at least 80 %, none below; no such bonus without it', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
const gx = elite && mod === GX;
|
||||
if (gx) {
|
||||
assert.equal(modOf(tier, GX).traitOverride.moduleDesc, '生命值高于80%时,技力自然回复速度+0.25/秒');
|
||||
assert.deepEqual(modOf(tier, GX).talentChanges.find((t) => t.talentIndex === -1).bb, { sp_recovery_per_sec: 0.25 });
|
||||
}
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, gx ? 1.25 : 1, `${label(f)}: full HP`);
|
||||
const sp0 = u.skill.sp;
|
||||
h.run(2);
|
||||
approx(u.skill.sp - sp0, 2 * (gx ? 1.25 : 1), `${label(f)}: SP over 2 s`, 0.02);
|
||||
u.hp = u.s.maxHp * 0.79;
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, 1, `${label(f)}: below 80 %`);
|
||||
u.hp = u.s.maxHp * 0.8;
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, gx ? 1.25 : 1, `${label(f)}: at 80 %`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 混合药物射击: every attack on an enemy rolls one of four with equal weight — heal 15 % max HP (×T2), that attack ×1.5, 停顿 1.4 s, 晕眩 1 s; GEE-X stage 3: 20 % / ×1.65 / 1.6 s / 1.2 s and 30 % all four at once', () => {
|
||||
for (const f of [[5, false, null], [6, true, null], [5, true, GX], [6, true, GX], [6, true, GY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = talentOf(tier, elite, mod, 0), t1 = talentOf(tier, elite, mod, 1);
|
||||
const x3 = elite && mod === GX && tier === 6;
|
||||
assert.deepEqual([t0.hp_ratio, t0.atk_scale, t0.sluggish, t0.stun, t0.prob ?? 0], x3 ? [0.2, 1.65, 1.6, 1.2, 0.3] : [0.15, 1.5, 1.4, 1, 0], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 7 });
|
||||
noSkill(h, u);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.run(400);
|
||||
const hits = atkHits(h, u).filter((c) => c.target === e);
|
||||
const n = hits.length;
|
||||
assert.ok(n > 250, `${label(f)}: ${n} attacks`);
|
||||
const atk = u.s.atk;
|
||||
const boosted = hits.filter((c) => Math.abs(c.amount - atk * t0.atk_scale) < 1e-6);
|
||||
assert.equal(boosted.length + hits.filter((c) => Math.abs(c.amount - atk) < 1e-6).length, n, `${label(f)}: ×1 or ×${t0.atk_scale}`);
|
||||
const st = h.hooksOf('statusApplied').filter((c) => c.source === u && c.target === e);
|
||||
const slug = st.filter((c) => c.status === 'sluggish'), stun = st.filter((c) => c.status === 'stun');
|
||||
for (const c of slug) approx(c.duration, t0.sluggish, `${label(f)}: 停顿`);
|
||||
for (const c of stun) approx(c.duration, t0.stun, `${label(f)}: 晕眩`);
|
||||
const heals = h.hooksOf('heal').filter((c) => c.source === u && c.target === u && !c.opts?.regen);
|
||||
for (const c of heals.slice(0, 5)) approx(c.amount, u.s.maxHp * t0.hp_ratio * t1.heal_scale, `${label(f)}: heal ${t0.hp_ratio * 100} % × ${t1.heal_scale}`, 1e-6);
|
||||
const share = x3 ? 0.3 + 0.7 / 4 : 0.25;
|
||||
for (const [what, list] of [['×atk', boosted], ['停顿', slug], ['晕眩', stun], ['heal', heals]]) {
|
||||
assert.ok(Math.abs(list.length / n - share) < 0.07, `${label(f)}: ${what} ${list.length} / ${n} ≈ ${share}`);
|
||||
}
|
||||
// all four at once: only GEE-X stage 3 — the boosted attacks that also stun, slow and heal in the same tick
|
||||
const at = (list) => new Set(list.map((c) => c.t.toFixed(4)));
|
||||
const sS = at(slug), sT = at(stun), sH = at(heals);
|
||||
const four = boosted.filter((c) => sS.has(c.t.toFixed(4)) && sT.has(c.t.toFixed(4)) && sH.has(c.t.toFixed(4))).length;
|
||||
if (x3) assert.ok(Math.abs(four / n - 0.3) < 0.07, `${label(f)}: all four ${four} / ${n} ≈ 30 %`);
|
||||
else assert.equal(four, 0, `${label(f)}: never two at once`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 药剂扩散: healing he receives ×1.2 (GEE-Y stage 3: ×1.25); GEE-Y stage 3 heals one ally of his range at each deployment for 150 % ATK — the straight line ahead first, never a 禁疗 one', () => {
|
||||
const others = [{ uid: 2, chessId: YAK, row: 10, col: 6 }, { uid: 3, chessId: TEXAS, row: 11, col: 4 }, { uid: 4, chessId: LISKAM, row: 10, col: 5 }];
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t1 = talentOf(tier, elite, mod, 1);
|
||||
const y3 = elite && mod === GY && tier === 6;
|
||||
assert.deepEqual([t1.heal_scale, t1['healinrange.heal_scale'] ?? 0], y3 ? [1.25, 1.5] : [1.2, 0], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, others });
|
||||
assert.equal(u.s.healingTakenMul, t1.heal_scale, `${label(f)}: ×${t1.heal_scale}`);
|
||||
const yak = h.unit(2), texas = h.unit(3), liskam = h.unit(4);
|
||||
// 雷蛇 on the line ahead is 禁疗 here: 角峰, two tiles ahead on the same line, beats 德克萨斯 beside him (nearer)
|
||||
h.b.addBuff(liskam, { key: 'test:noHeal', flags: { noHeal: true } });
|
||||
for (const a of [yak, texas, liskam]) a.hp = 100;
|
||||
h.b.kill(u, null);
|
||||
const n0 = h.hooksOf('heal').length;
|
||||
assert.ok(h.b.redeploy(u, { free: true }), 'redeployed');
|
||||
const heals = h.hooksOf('heal').slice(n0).filter((c) => c.source === u && c.target !== u);
|
||||
if (y3) {
|
||||
assert.deepEqual(heals.map((c) => c.target.uid), [2], `${label(f)}: 角峰, ahead on his line`);
|
||||
approx(heals[0].amount, u.s.atk * 1.5, `${label(f)}: 150 % ATK`);
|
||||
} else assert.equal(heals.length, 0, `${label(f)}: no deploy heal`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 快速射击 (MANUAL, data DEFAULT): ASPD +50 / +70 for 23 / 26 s, then back', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, GY]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.duration, sk.spCost, sk.initSp, sk.bb.attack_speed], [elite ? 26 : 23, 30, elite ? 17 : 16, elite ? 70 : 50]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
const aspd0 = u.s.aspd;
|
||||
u.skill.gainSp(999);
|
||||
h.run(2);
|
||||
assert.equal(u.skill.activations, 0, 'no enemy, no cast');
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast with an enemy in his range');
|
||||
approx(u.skill.timeLeft, sk.duration, 'duration', 0.02);
|
||||
assert.equal(u.s.aspd, aspd0 + sk.bb.attack_speed, 'ASPD');
|
||||
h.runUntil(() => !u.skill.active, sk.duration + 1);
|
||||
assert.equal(u.s.aspd, aspd0, 'back');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 爆发剂·γ型 (MANUAL, data DEFAULT, 30 s): 15 physical hits of 500 attack power on the ally of his range ahead (DEF applies, 5 % floor, 受击回复 SP), then max HP / DEF +40 % / +50 % on both for 30 s; the line ahead beats a nearer side ally', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, GX]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.duration, sk.spCost, sk.bb.damage, sk.bb.max_hp, sk.bb.def], [30, elite ? 34 : 37, 500, elite ? 0.5 : 0.4, elite ? 0.5 : 0.4]);
|
||||
assert.match(sk.desc, /攻击15次/);
|
||||
// 雷蛇 three tiles ahead on his line (DEF 489: 25 per hit, 5 %), 德克萨斯 diagonal-adjacent (nearer)
|
||||
const others = [{ uid: 2, chessId: LISKAM, row: 10, col: 7 }, { uid: 3, chessId: TEXAS, row: 11, col: 5 }];
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, others });
|
||||
const liskam = h.unit(2), texas = h.unit(3);
|
||||
assert.equal(liskam.skill.spType, 'hurt');
|
||||
liskam.skill.sp = 0;
|
||||
const hp0 = liskam.hp, max0 = liskam.s.maxHp, def0 = liskam.s.def;
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
|
||||
const shots = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.tags?.includes('haak:stim'));
|
||||
assert.equal(shots.length, 15, '15 hits');
|
||||
assert.ok(shots.every((c) => c.target === liskam && c.type === 'phys' && c.dmg.isSkill), '雷蛇, physical skill damage');
|
||||
for (const c of shots) approx(c.amount, Math.max(500 - def0, 25), 'max(500 − DEF, 5 %)');
|
||||
// 受击回复: +1 SP per hit (雷蛇's own 战术防御 "受到攻击时…回复…1点技力" answers each hit too)
|
||||
assert.equal(h.hooksOf('spGain').filter((c) => c.unit === liskam && c.reason === 'hurt').length, 15, '受击回复: +1 SP per hit');
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.target === texas).length, 0, '德克萨斯 untouched');
|
||||
const mods = { hpPct: sk.bb.max_hp, defPct: sk.bb.def };
|
||||
assert.deepEqual(u.findBuff('haak:s2:self')?.mods, mods, 'himself');
|
||||
assert.deepEqual(liskam.findBuff(`haak:s2:${u.id}`)?.mods, mods, 'the target');
|
||||
approx(liskam.findBuff(`haak:s2:${u.id}`).timeLeft, 30, 'for the skill duration', 0.02);
|
||||
approx(liskam.s.maxHp, max0 * (1 + sk.bb.max_hp), 'max HP');
|
||||
approx(liskam.hp, (hp0 - 15 * Math.max(500 - def0, 25)) * (1 + sk.bb.max_hp), 'the HP ratio kept');
|
||||
h.run(30.1);
|
||||
assert.ok(!u.findBuff('haak:s2:self') && !liskam.findBuff(`haak:s2:${u.id}`), 'gone after 30 s');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('爆发剂 edge cases: a low-DEF ally can be knocked out by the volley (he still gets his part); with no ally in his range the cast does nothing', () => {
|
||||
{
|
||||
const { h, u } = field({ tier: 5, skill: 1, others: [{ uid: 2, chessId: TEXAS, row: 10, col: 5 }] });
|
||||
const texas = h.unit(2);
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
const shots = h.hooksOf('damaged').filter((c) => c.source === u && c.target === texas);
|
||||
assert.ok(!texas.alive && shots.length < 15, `德克萨斯 (DEF 260) knocked out after ${shots.length} hits`);
|
||||
assert.ok(u.findBuff('haak:s2:self'), 'his own part');
|
||||
done(h);
|
||||
}
|
||||
{
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2, others: [{ uid: 2, chessId: YAK, row: 10, col: 3 }] }); // behind him
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast: the DEFAULT rule needs an enemy only');
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.dmg?.tags?.includes('haak:stim')).length, 0, 'no volley');
|
||||
assert.equal(u.findBuff('haak:s3:self'), null, 'no buff');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 爆发剂·榴莲味 (MANUAL, data DEFAULT, 20 s): the same volley, then ATK +30 % / +35 % and ASPD +30 / +35 on both for 20 s', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, GY]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.duration, sk.spCost, sk.bb.damage, sk.bb.atk, sk.bb.attack_speed], [20, elite ? 41 : 44, 500, elite ? 0.35 : 0.3, elite ? 35 : 30]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: YAK, row: 9, col: 5 }] });
|
||||
const yak = h.unit(2);
|
||||
const atk0 = yak.s.atk, aspd0 = yak.s.aspd, myAtk = u.s.atk, myAspd = u.s.aspd;
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast');
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.target === yak && c.dmg?.tags?.includes('haak:stim')).length, 15, '角峰 (beside, ahead diagonal) takes the volley');
|
||||
approx(yak.s.atk, atk0 * (1 + sk.bb.atk), 'its ATK');
|
||||
assert.equal(yak.s.aspd, aspd0 + sk.bb.attack_speed, 'its ASPD');
|
||||
approx(u.s.atk, myAtk * (1 + sk.bb.atk), 'his ATK');
|
||||
assert.equal(u.s.aspd, myAspd + sk.bb.attack_speed, 'his ASPD');
|
||||
approx(u.findBuff('haak:s3:self').timeLeft, 20, '20 s', 0.02);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
// test/content/op_phatom.test.js — the 自选 operator kit of 傀影 (char_250_phatom, 6★ 处决者; kit
|
||||
// server/sim/content/kits/ops/op-phatom.js) and of his summon 镜中虚影 (token_10007_phatom_twin), fielded the production way
|
||||
// (a DIY slot + its `diy` pick, simdata getDiy; the twin as a placed token piece `{ kind: 'token', tokenId, ownerUid }`) in
|
||||
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, EXE-X
|
||||
// 克里斯汀小姐的毛毡, EXE-Y “如影随形” or ISW-A 傀影特限证章 at stage 1 (tier 5) / 3 (tier 6). Every number is read back from
|
||||
// data/backups.json (the form of that slot status, the token variant of that owner form); the fidelity checklist of
|
||||
// kits/README.md item by item.
|
||||
// Run: node --test test/content/op_phatom.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 { TWIN } from '../../server/sim/content/kits/ops/op-phatom.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 PHATOM = 'char_250_phatom';
|
||||
const FORMS = BACKUPS.units[PHATOM].forms;
|
||||
const TOKEN = BACKUPS.tokens[TWIN];
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const EX = 'uniequip_002_phatom', EY = 'uniequip_003_phatom', IS = 'uniequip_004_phatom';
|
||||
const S1 = 'skchr_phatom_1', S2 = 'skchr_phatom_2', S3 = 'skchr_phatom_3';
|
||||
const TEXAS = 'chess_char_1_08_a';
|
||||
const statusKey = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusKey(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
/** The twin's variant of an owner form (+ its module part). */
|
||||
const twinOf = (tier, elite, mod) => {
|
||||
const v = TOKEN.variants[`${PHATOM}@${statusKey(tier, elite)}`];
|
||||
const bm = elite && mod ? v.byModule?.[mod] : null;
|
||||
return { stats: bm?.stats ?? v.stats, talents: bm?.talents ?? v.talents, skillOf: (i) => (i === 0 ? v.skill : v.bySkill[i].skill) };
|
||||
};
|
||||
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_heavy: dummy('enemy_heavy', { mass: 9 }), enemy_evade: dummy('enemy_evade'),
|
||||
};
|
||||
/** Every 自选 form: [tier, elite, module]. */
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, EX, EY, IS].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** A battle with 傀影 as uid 1 at (row, col) facing RIGHT, his twin piece (uid 2) at `twinAt` unless null, plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, twinAt = [12, 4], others = [], seed = 5, dp = 0 } = {}) {
|
||||
const units = [{ uid: 1, diy: { slot: SLOT[tier], charId: PHATOM, skillIndex: skill, uniEquipId: mod }, elite, row, col }];
|
||||
if (twinAt) units.push({ uid: 2, kind: 'token', tokenId: TWIN, ownerUid: 1, row: twinAt[0], col: twinAt[1] });
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'statusApplied', 'death', 'deploy', 'dodge'], captureNoisy: true,
|
||||
units: [...units, ...others],
|
||||
});
|
||||
h.step();
|
||||
h.b.players[0].dp = dp;
|
||||
return { h, u: h.unit(1), t: twinAt ? h.unit(2) : null };
|
||||
}
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Knock `u` out and bring it straight back on its tile (free). */
|
||||
function redeployNow(h, u) {
|
||||
h.b.kill(u, null);
|
||||
assert.ok(h.b.redeploy(u, { free: true }), `${u.name} redeployed`);
|
||||
}
|
||||
|
||||
test('傀影 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, blocks 1, melee ground-only, 18 s redeploy, 维多利亚, no 特质; his placed 镜中虚影 deploys after him with the variant\'s stats and its copy of his skill', () => {
|
||||
assert.equal(OPERATOR_KITS[PHATOM], KITS[PHATOM]);
|
||||
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, tw = twinOf(tier, elite, mod);
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [PHATOM, SLOT[tier], form.skills[skill].skillId, 'passive', false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd, u.base.respawnTime, u.base.cost],
|
||||
[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), 18, 10], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly], [1, 'melee', 'phys', false], `${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 him`);
|
||||
// the twin
|
||||
assert.deepEqual([t.defId, t.ownerUnit === u, t.alive, t.deployed, t.deploySeq > u.deploySeq, !!t.kit.generic], [TWIN, true, true, true, true, false], `${label(f)}: twin deployed after him`);
|
||||
assert.deepEqual([t.base.maxHp, t.base.atk, t.base.def, t.base.respawnTime, t.base.cost], [tw.stats.maxHp, tw.stats.atk, tw.stats.def, 45, 5], `${label(f)}: twin stats`);
|
||||
assert.deepEqual([t.def.skill.id, t.skill.kind], [tw.skillOf(skill).skillId, 'passive'], `${label(f)}: twin skill copy`);
|
||||
assert.deepEqual([t.s.blockCnt, t.profile.attack, t.profile.canHitFly, t.liveRangeGrid], [1, 'melee', false, [[0, 0], [0, 1]]], `${label(f)}: twin executor`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the numbers (zh_CN): 傀影 E2 Lv1 1283 / 435 / 270, E2 Lv60 1523 / 517 / 304; the twin 1126 / 429 / 226 → 1318 / 508 / 254,
|
||||
// EXE-Y stage 3 "属性进一步增强" 1418 / 568 / 294; modules
|
||||
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], [1283, 435, 1523, 517]);
|
||||
assert.deepEqual([twinOf(5, false).stats.maxHp, twinOf(5, true).stats.atk, twinOf(6, true, EY).stats.maxHp, twinOf(6, true, EY).stats.atk, twinOf(5, true, EY).stats.atk], [1126, 508, 1418, 568, 508]);
|
||||
assert.deepEqual([modOf(5, EX).attr, modOf(6, EX).attr, modOf(5, EY).attr, modOf(6, EY).attr, modOf(5, IS).attr, modOf(6, IS).attr],
|
||||
[{ maxHp: 100, atk: 50 }, { maxHp: 140, atk: 73 }, { atk: 40, def: 24 }, { atk: 75, def: 30 }, { atk: 37, aspd: 5 }, { atk: 71, aspd: 7 }]);
|
||||
});
|
||||
|
||||
test('a 自选 pick: 傀影 is offered at tiers 5 and 6; a roster with EXE-X / EXE-Y passes validateDiyPicks, his 集成战略 module ISW-A is refused', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(PHATOM));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(PHATOM), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: PHATOM, skillIndex: 2, uniEquipId: EY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: PHATOM, skillIndex: 2, uniEquipId: EY } } });
|
||||
const bad = validateDiyPicks({ [SLOT[5]]: { charId: PHATOM, skillIndex: 0, uniEquipId: IS } }, { data, kitted: KITTED_CHARS });
|
||||
assert.equal(bad.error, 'BAD_TARGET');
|
||||
assert.match(bad.detail, /集成战略/);
|
||||
});
|
||||
|
||||
test('S1 暗夜魅影 (被动): at each deployment 30 % / 40 % physical dodge and a 屏障 of 35 % / 50 % max HP that absorbs physical damage only, both 10 s; the twin\'s copy on itself', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EY]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.skillType, sk.spType, sk.bb.prob, sk.bb.hp_ratio, sk.bb.duration], ['PASSIVE', 'ON_DEPLOY', elite ? 0.4 : 0.3, elite ? 0.5 : 0.35, 10]);
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0 });
|
||||
for (const x of [u, t]) {
|
||||
const b = x === u ? sk.bb : twinOf(tier, elite, mod).skillOf(0).bb;
|
||||
approx(x.s.dodgePhys, b.prob, `${x.name}: physical dodge`);
|
||||
assert.equal(x.s.dodgeArts, 0, `${x.name}: no arts dodge`);
|
||||
const sh = x.buffs.find((y) => y.shield > 0);
|
||||
approx(sh.shield, x.s.maxHp * b.hp_ratio, `${x.name}: ${b.hp_ratio * 100} % max HP`);
|
||||
assert.equal(sh.shieldType, 'phys', `${x.name}: physical only`);
|
||||
}
|
||||
// arts damage passes the barrier, physical damage is absorbed
|
||||
const hp0 = u.hp;
|
||||
h.b.dealDamage(null, u, { amount: 100, type: 'arts', canDodge: false });
|
||||
approx(u.hp, hp0 - 100, 'arts passes');
|
||||
h.b.dealDamage(null, u, { amount: 100, type: 'phys', canDodge: false });
|
||||
approx(u.hp, hp0 - 100, 'physical absorbed');
|
||||
h.run(10.2);
|
||||
assert.ok(u.s.dodgePhys === 0 && !u.buffs.some((y) => y.shield > 0), 'gone after 10 s');
|
||||
redeployNow(h, u);
|
||||
approx(u.s.dodgePhys, sk.bb.prob, 'again at a redeployment');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 血色乐章 (被动): at each deployment 8 / 9 stacks of ATK +13 % / +16 %; each damage instance he deals uses one (a dodged attack none); the twin\'s copy has its own', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EX]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.bb.times, sk.bb.atk], [elite ? 9 : 8, elite ? 0.16 : 0.13]);
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 1 });
|
||||
assert.equal(u.findBuff('phatom:skill').stacks, sk.bb.times);
|
||||
assert.equal(t.findBuff('phatom:twin:skill').stacks, sk.bb.times, 'the twin\'s own stacks');
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.times * sk.bb.atk + (u.findBuff('trait:phatom:lonely') ? 0.1 : 0)), 'ATK');
|
||||
// an enemy that dodges everything: no stack used
|
||||
const ev = h.spawn('enemy_evade', { pos: [10, 5] });
|
||||
h.b.addBuff(ev, { key: 'test:dodge', mods: { dodgePhys: 1 } });
|
||||
h.run(3);
|
||||
assert.ok(h.hooksOf('dodge').some((c) => c.source === u), 'dodged attacks');
|
||||
assert.equal(u.findBuff('phatom:skill').stacks, sk.bb.times, 'none used');
|
||||
h.b.removeBuff(ev, 'test:dodge');
|
||||
const fullAtk = u.s.atk;
|
||||
const n0 = h.hooksOf('damaged').filter((c) => c.source === u).length;
|
||||
h.run(0.95);
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === u).slice(n0);
|
||||
assert.ok(hits.length >= 1, 'hits');
|
||||
assert.equal(u.findBuff('phatom:skill').stacks, sk.bb.times - hits.length, 'one per damage instance');
|
||||
approx(hits[0].amount, fullAtk, 'the first hit at full stacks (a 0-DEF target)');
|
||||
approx(u.s.atk, fullAtk - u.base.atk * sk.bb.atk * hits.length, 'ATK −atk per stack used');
|
||||
h.run(30);
|
||||
assert.equal(u.findBuff('phatom:skill'), null, 'all used');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 夜幕突袭 (被动): at each deployment 210 % / 240 % ATK physical to every ground enemy of the x-4, a radial 小力 push and ONE status for all — 停顿 / 晕眩 / 束缚 2.5 / 3 s (each drawn over the casts); flyers and enemies outside untouched; the twin\'s copy at its deployment', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, EY]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.rangeId, sk.bb.atk_scale, sk.bb.force, sk.bb.sluggish, sk.bb.root, sk.bb.stun], ['x-4', elite ? 2.4 : 2.1, 0, elite ? 3 : 2.5, elite ? 3 : 2.5, elite ? 3 : 2.5]);
|
||||
const drawn = new Set();
|
||||
for (const seed of [1, 2, 3, 4, 5, 6, 7, 8]) {
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 2, seed, twinAt: [11, 6] });
|
||||
const ahead = h.spawn('enemy_dummy', { pos: [10, 5] }), diag = h.spawn('enemy_dummy', { pos: [9, 3] });
|
||||
const heavy = h.spawn('enemy_heavy', { pos: [11, 4] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 4] }), far = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.step(); // the enemy tile index
|
||||
const n0 = h.hooksOf('damaged').length, s0 = h.hooksOf('statusApplied').length;
|
||||
redeployNow(h, u);
|
||||
const hits = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.dmg?.tags?.includes('phatom:raid'));
|
||||
assert.deepEqual(hits.map((c) => c.target).sort((a, b) => a.id - b.id), [ahead, diag, heavy].sort((a, b) => a.id - b.id), `seed ${seed}: the ground enemies of his x-4`);
|
||||
for (const c of hits) {
|
||||
approx(c.amount, u.s.atk * sk.bb.atk_scale, `seed ${seed}: ${sk.bb.atk_scale * 100} % ATK`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true]);
|
||||
}
|
||||
assert.ok(!hits.some((c) => c.target === fly || c.target === far), 'flyer / outside untouched');
|
||||
assert.ok(ahead.x > 5.5, `seed ${seed}: pushed away (radial, 小力)`);
|
||||
approx(heavy.y, 11, `seed ${seed}: 重量 9 — 小力 moves nothing`);
|
||||
const st = h.hooksOf('statusApplied').slice(s0).filter((c) => c.source === u);
|
||||
assert.equal(new Set(st.map((c) => c.status)).size, 1, `seed ${seed}: one status for the cast`);
|
||||
assert.equal(st.length, 3, `seed ${seed}: on each of the three`);
|
||||
for (const c of st) approx(c.duration, sk.bb[c.status === 'bind' ? 'root' : c.status], `seed ${seed}: ${c.status}`);
|
||||
drawn.add(st[0].status);
|
||||
// the twin's copy at its own deployment
|
||||
const tn0 = h.hooksOf('damaged').length;
|
||||
redeployNow(h, t);
|
||||
const th = h.hooksOf('damaged').slice(tn0).filter((c) => c.source === t && c.dmg?.tags?.includes('phatom:raid'));
|
||||
assert.ok(th.length >= 1 && th.every((c) => Math.abs(c.amount - t.s.atk * twinOf(tier, elite, mod).skillOf(2).bb.atk_scale) < 1e-6), `seed ${seed}: the twin's burst`);
|
||||
done(h);
|
||||
}
|
||||
assert.deepEqual([...drawn].sort(), ['bind', 'sluggish', 'stun'], 'all three statuses over the casts');
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 镜中虚影 / T2 虚影精通: the twin is killed when he leaves the field and comes back on its tile 35 s (45 − 10; EXE-X stage 3: 45 − 16) after its LAST deployment, paying 5 DP, only while he stands', () => {
|
||||
for (const f of [[5, false, null], [5, true, EX], [6, true, EX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const cut = twinOf(tier, elite, mod).talents.find((x) => x.bb.respawn_time != null).bb.respawn_time;
|
||||
assert.equal(cut, tier === 6 && mod === EX ? -16 : -10, label(f));
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 1, dp: 50 });
|
||||
const wait = 45 + cut;
|
||||
// knocked out while he stands: back `wait` s after its deployment at 0 s, 5 DP paid
|
||||
h.run(5);
|
||||
h.b.kill(t, null);
|
||||
assert.ok(!t.alive && !t.removed, `${label(f)}: the piece stays`);
|
||||
h.run(wait - 5 - 1);
|
||||
assert.ok(!t.alive, `${label(f)}: not before ${wait} s`);
|
||||
assert.ok(h.runUntil(() => t.alive, 1.5), `${label(f)}: back at ${wait} s`);
|
||||
assert.deepEqual([t.tileR, t.tileC, h.b.players[0].dp], [12, 4, 45], `${label(f)}: its tile, 5 DP`);
|
||||
const at = h.b.time;
|
||||
// he is knocked out: the twin is killed with him (KillTokens) and waits for him
|
||||
h.b.kill(u, null);
|
||||
u.respawnAt = Infinity; // he stays down until the test brings him back
|
||||
assert.ok(!t.alive, `${label(f)}: killed with him`);
|
||||
h.run(wait + 1);
|
||||
assert.ok(!t.alive && !u.alive, `${label(f)}: no return while he is down`);
|
||||
assert.ok(h.b.redeploy(u, { free: true }));
|
||||
assert.ok(h.runUntil(() => t.alive, 0.5), `${label(f)}: back with him once its own timer (from ${at.toFixed(1)} s) has run`);
|
||||
// no DP: it waits
|
||||
h.b.kill(t, null);
|
||||
h.b.players[0].dp = 0;
|
||||
h.run(wait + 1);
|
||||
assert.ok(!t.alive, `${label(f)}: no DP, no return`);
|
||||
h.b.players[0].dp = 5;
|
||||
assert.ok(h.runUntil(() => t.alive, 0.5), `${label(f)}: back with the DP`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('EXE-X / ISW-A: EXE-X\'s refund has no effect (no retreat in battle) — only its attributes and T2 at stage 3; ISW-A acts by its attributes only (集成战略-only trait and talent)', () => {
|
||||
{
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: EX, skill: 0, dp: 7 });
|
||||
assert.deepEqual(u.def.traitBb, { withdraw_cost_recover_ratio: 0.8 });
|
||||
h.b.retreat(u);
|
||||
h.step();
|
||||
assert.equal(h.b.players[0].dp, 7, 'no refund');
|
||||
done(h);
|
||||
}
|
||||
{
|
||||
const { h, u, t } = field({ tier: 6, elite: true, mod: IS, skill: 1, dp: 50 });
|
||||
assert.match(modOf(6, IS).traitOverride.moduleDesc, /^在集成战略中/);
|
||||
assert.ok(modOf(6, IS).talentChanges.filter((x) => x.desc).every((x) => /在集成战略中/.test(x.desc)));
|
||||
assert.equal(u.findBuff('module:phatom:both'), null, 'no ATK while both stand');
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.run(5);
|
||||
h.b.kill(t, null);
|
||||
assert.ok(!h.runUntil(() => t.alive, 29), 'its attacks shorten no redeploy time');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('EXE-Y “如影随形”: ATK +10 % while no allied operator stands on the 4 tiles beside him (the twin does not count); stage 3: ATK +10 % on him and the twin while both are on the field', () => {
|
||||
for (const f of [[5, true, EY], [6, true, EY], [6, true, null]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const ey = mod === EY, y3 = ey && tier === 6;
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, twinAt: [10, 5], others: [{ uid: 3, chessId: TEXAS, row: 12, col: 7 }] });
|
||||
const texas = h.unit(3);
|
||||
assert.equal(!!u.findBuff('trait:phatom:lonely'), ey, `${label(f)}: the twin beside him does not count`);
|
||||
if (ey) assert.deepEqual(u.findBuff('trait:phatom:lonely').mods, { atkPct: 0.1 });
|
||||
assert.equal(!!u.findBuff('module:phatom:both'), y3, `${label(f)}: both on the field`);
|
||||
assert.equal(!!t.findBuff('module:phatom:both'), y3, `${label(f)}: the twin too`);
|
||||
if (y3) assert.match(FORMS['2/60/7/3'].modules.find((m) => m.uniEquipId === EY).talentChanges[0].desc, /本体和虚影同时在场时,攻击力各\+10%/);
|
||||
// an operator beside him
|
||||
assert.ok(h.b.relocate(texas, 11, 4), 'moved beside him');
|
||||
h.step();
|
||||
assert.equal(u.findBuff('trait:phatom:lonely'), null, `${label(f)}: an operator beside`);
|
||||
// the twin down: the shared ATK goes
|
||||
h.b.kill(t, null);
|
||||
h.step();
|
||||
assert.equal(u.findBuff('module:phatom:both'), null, `${label(f)}: twin down`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
+386
-99
@@ -3993,126 +3993,132 @@
|
||||
"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=haak/S1 5_diy2_a=haak/S2 5_diy1_a=haak/S3 5_diy2_b=haak/S1 5_diy1_b=haak/S2 5_diy2_b=haak/S3 5_diy1_b=haak/S1/002_haak 5_diy2_b=haak/S2/002_haak 5_diy1_b=haak/S3/002_haak 5_diy2_b=haak/S1/003_haak 5_diy1_b=haak/S2/003_haak 5_diy2_b=haak/S3/003_haak",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m06",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 12,
|
||||
"seed": 3107422628,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 201.93,
|
||||
"ticks": 6058,
|
||||
"time": 203.03,
|
||||
"ticks": 6091,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 90,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"rngDraws": 401,
|
||||
"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":145950,"bossDmg":0,"heal":28415,"deaths":27,"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,1578.19,1,118,0,6531,4149,846,30,1],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,91,0,6355,7197,3482,48,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,108,0,3954,6589,1708,21,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,117,0,5614,7948,2257,34,1],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,113,0,13703,8214,1688,24,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,57,0,2658,3965,171,14,0],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_haak",0,0,82,0,40035,8663,4604,57,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_haak",0,0,99,0,7655,8348,4145,35,1],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_haak",0,0,56,0,5855,6591,2560,28,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_haak",0,0,69,0,35756,8783,4989,64,2],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_haak",0,0,71,0,14910,5881,1280,31,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_haak",0,0,51,0,2923,4477,683,15,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,18458,125236],"enemy_1119_vofsd":[39,0,39,25107,14606],"enemy_1121_lifbos_2":[24,0,24,13835,6108]},
|
||||
"hooks": [1,118,6091,474,474,514,1070,1972,27,117,15,15,0,9663,196,21,90,90,1,27,0,0,0],
|
||||
"events": {"atk":474,"deploy":28,"die":27,"dmg":514,"fx":118,"heal":101,"leak":90,"skill":30,"spawn":102,"status":452},
|
||||
"fx": {"crit":102,"liberate":9,"volley":7},
|
||||
"snaps": "5bf1cd4b 2c4339fe daacd836 2c359d68 9f186274 24f5b608 f405dbc1 79082ff4 0ef2ea97 b7ad1541 faa3d42e 256ed72c b28670a9 b8d7e9ea 79e44953 f235112f 83307ea8 452e421e 91664117 b258ae3b 83318e42"
|
||||
},
|
||||
"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=haak/S1 6_diy2_a=haak/S2 6_diy1_a=haak/S3 6_diy2_b=haak/S1 6_diy1_b=haak/S2 6_diy2_b=haak/S3 6_diy1_b=haak/S1/002_haak 6_diy2_b=haak/S2/002_haak 6_diy1_b=haak/S3/002_haak 6_diy2_b=haak/S1/003_haak 6_diy1_b=haak/S2/003_haak 6_diy2_b=haak/S3/003_haak",
|
||||
"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": 203.23,
|
||||
"ticks": 6097,
|
||||
"reason": "cleared",
|
||||
"killed": 65,
|
||||
"total": 90,
|
||||
"killed": 61,
|
||||
"total": 159,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 215,
|
||||
"rngDraws": 906,
|
||||
"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":61,"total":159,"perfect":false,"leaks":98,"leaksUncounted":0,"coins":0,"dmg":302021,"bossDmg":0,"heal":21556,"deaths":18,"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,1464.3,1,151,4,4965,2717,732,30,2],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,1252.79,1,7,8,17989,3889,3423,102,3],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1122.63,1,168,4,4156,4198,439,29,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,153,5,24425,5387,1593,80,2],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,170,7,22151,6725,2030,65,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",1897,1,6,13,76840,3851,3851,164,4],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_haak",0,0,101,5,10176,5300,1026,40,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_haak",0,0,157,5,53306,8719,3483,97,3],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_haak",0,0,99,1,61461,6069,1795,75,2],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_haak",1840.09,1,186,0,14609,6222,512,27,0],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_haak",294.7,1,3,6,9566,4249,2134,47,2],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_haak",1707.3,1,185,3,2377,4325,537,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]
|
||||
],
|
||||
"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,6,9,0,27144],
|
||||
"enemy_1197_sfshu_2": [39,16,23,0,86380],
|
||||
"enemy_1198_msfshu": [18,12,6,0,42],
|
||||
"enemy_1198_msfshu_2": [48,21,27,0,99],
|
||||
"enemy_9010_acpupp": [39,6,33,0,188356]
|
||||
},
|
||||
"hooks": [1,186,6097,774,774,867,2250,229,79,177,23,22,0,22372,488,0,159,98,1,86,0,0,0],
|
||||
"events": {"atk":774,"deploy":25,"die":79,"dmg":846,"fx":297,"heal":221,"leak":98,"skill":45,"spawn":173,"status":703},
|
||||
"fx": {"crit":235,"ember":7,"heal":4,"revive":1,"shieldBreak":3,"summon":22,"telegraph":7,"volley":18},
|
||||
"snaps": "33efed90 c6f03965 e55815eb e019c653 5b426217 56158b14 ad6915cd 068a0579 bb30bc11 3b4fc571 140e47e4 b2e9ba62 75c8b96d d611616f a97f1259 bf66deff ac9197fc f75b3e7c 1869228a ab42d7db 33846ae1"
|
||||
},
|
||||
"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=phatom/S1 5_diy2_a=phatom/S2 5_diy1_a=phatom/S3 5_diy2_b=phatom/S1 5_diy1_b=phatom/S2 5_diy2_b=phatom/S3 5_diy1_b=phatom/S1/002_phatom 5_diy2_b=phatom/S2/002_phatom 5_diy1_b=phatom/S3/002_phatom 5_diy2_b=phatom/S1/003_phatom 5_diy1_b=phatom/S2/003_phatom 5_diy2_b=phatom/S3/003_phatom",
|
||||
"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": 226.93,
|
||||
"ticks": 6808,
|
||||
"reason": "cleared",
|
||||
"killed": 47,
|
||||
"total": 150,
|
||||
"killed": 14,
|
||||
"total": 102,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 398,
|
||||
"rngDraws": 21,
|
||||
"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":14,"total":102,"perfect":false,"leaks":88,"leaksUncounted":0,"coins":0,"dmg":67474,"bossDmg":0,"heal":0,"deaths":33,"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,0,0,129,2,13201,7698,0,60,0],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1283,1,137,2,9575,8981,0,39,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1283,1,136,2,8531,7698,0,31,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,135,0,388,3046,0,15,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,44,2,8406,3046,0,17,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,5,0,103,1523,0,4,0],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_phatom",0,0,46,2,7940,3246,0,21,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_phatom",0,0,1,1,4903,1623,0,5,0],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_phatom",0,0,2,1,3899,1623,0,8,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_phatom",0,0,6,0,111,1523,0,4,0],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_phatom",0,0,38,2,9493,3046,0,15,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_phatom",0,0,31,0,923,3046,0,10,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,14,49,1600,57844],"enemy_1174_duholy_2":[15,0,15,14453,2821],"enemy_1175_dushdo_2":[24,0,24,30045,6808]},
|
||||
"hooks": [1,137,6808,375,375,384,380,0,47,135,0,33,0,0,8,47,102,88,1,47,0,0,4],
|
||||
"events": {"atk":375,"deploy":35,"die":47,"dmg":376,"fx":43,"leak":88,"skill":70,"spawn":114,"status":144},
|
||||
"fx": {"aoe":11,"buff":13,"displace":4,"dodge":4,"shield":11},
|
||||
"snaps": "f028d3c9 0c70519d fb7aa711 051d1381 9063f906 865d4860 c868675b 1239f5a8 fa81caf8 ba1fc818 7d7e3c4c 03b46d7a deaed47c 7b186e3f ec95ad21 253244b8 dc57db23 17b1717b 17b4f977 a8fa7369 7987afbf ebcb1b37 b915a5c0"
|
||||
},
|
||||
"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": "5_diy1_b=phatom/S1/004_phatom 5_diy2_b=phatom/S2/004_phatom 5_diy1_b=phatom/S3/004_phatom 6_diy1_a=phatom/S1 6_diy2_a=phatom/S2 6_diy1_a=phatom/S3 6_diy2_b=phatom/S1 6_diy1_b=phatom/S2 6_diy2_b=phatom/S3 6_diy1_b=phatom/S1/002_phatom 6_diy2_b=phatom/S2/002_phatom 6_diy1_b=phatom/S3/002_phatom",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m02",
|
||||
@@ -4120,37 +4126,318 @@
|
||||
"round": 13,
|
||||
"seed": 3733794685,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 6,
|
||||
"time": 217.8,
|
||||
"ticks": 6534,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 105,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 913,
|
||||
"rngDraws": 27,
|
||||
"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":0,"total":105,"perfect":false,"leaks":105,"leaksUncounted":0,"coins":0,"dmg":103075,"bossDmg":0,"heal":0,"deaths":32,"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_5_diy1_b",0,"uniequip_004_phatom",0,0,138,0,11053,7615,0,96,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_004_phatom",0,0,140,0,19557,9138,0,75,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_004_phatom",1523,1,141,0,21515,7615,0,47,0],
|
||||
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,134,0,6234,3849,0,43,0],
|
||||
["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,114,0,8292,3849,0,24,0],
|
||||
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,40,0,12165,2566,0,30,0],
|
||||
["p1",7,"op","chess_char_6_diy2_b",0,"none",0,0,35,0,672,3046,0,26,0],
|
||||
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,41,0,10143,3046,0,21,0],
|
||||
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,38,0,1705,3046,0,28,0],
|
||||
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_phatom",0,0,9,0,531,1663,0,18,0],
|
||||
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_phatom",0,0,10,0,4952,1663,0,4,0],
|
||||
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_phatom",1663,1,6,0,6256,0,0,81,0],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_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_1116_liprr_2":[27,0,27,12887,68302],"enemy_1119_vofsd":[48,0,48,17936,27169],"enemy_1121_lifbos_2":[30,0,30,16273,7604]},
|
||||
"hooks": [1,141,6534,592,592,616,609,1113,32,137,0,32,0,0,24,32,105,105,1,32,0,0,7],
|
||||
"events": {"atk":592,"deploy":34,"die":32,"dmg":607,"fx":66,"leak":105,"skill":66,"spawn":119,"status":195},
|
||||
"fx": {"aoe":11,"buff":12,"displace":4,"dodge":7,"liberate":21,"shield":11},
|
||||
"snaps": "af8e52b0 dee72db9 e71eea50 5a551e0b 8613bcbe bc88904f f6f05fd7 f3ec6f71 ff754a67 40e39b81 5a81b776 55b9b8c2 7e0bdfea 16c6a3d8 452233d7 ef18be46 4d7b9c55 4e3e4d25 7107d706 ee5aefb7 7bcf87ed 74023ee2"
|
||||
},
|
||||
"diy-098": {
|
||||
"about": "6_diy1_b=phatom/S1/003_phatom 6_diy2_b=phatom/S2/003_phatom 6_diy1_b=phatom/S3/003_phatom 6_diy2_b=phatom/S1/004_phatom 6_diy1_b=phatom/S2/004_phatom 6_diy2_b=phatom/S3/004_phatom 5_diy1_a=crosly/S1 5_diy2_a=crosly/S2 5_diy1_a=crosly/S3 5_diy2_b=crosly/S1 5_diy1_b=crosly/S2 5_diy2_b=crosly/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": 0,
|
||||
"total": 66,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 19,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":66,"perfect":false,"leaks":66,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":0,"deaths":29,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_003_phatom",1523,1,101,0,0,9138,0,0,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_003_phatom",1049.41,1,102,0,0,12658,0,0,0],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_003_phatom",1523,1,10,0,0,0,0,0,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_004_phatom",1523,1,103,0,0,6092,0,0,0],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_004_phatom",1523,1,104,0,0,3046,0,0,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_004_phatom",0,0,38,0,0,3046,0,0,0],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,11,0,0,1322,0,0,1],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,30,0,0,2644,0,0,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,5,0,0,1322,0,0,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,7,0,0,1569,0,0,1],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",1569,1,9,0,0,0,0,0,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,37,0,0,3138,0,0,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[12,0,12,2278,0],"enemy_1041_lazerd_2":[24,0,24,41696,0],"enemy_1042_frostd":[30,0,30,0,0]},
|
||||
"hooks": [1,104,6884,133,133,91,87,0,29,95,8,30,0,0,0,0,66,66,1,29,0,0,4],
|
||||
"events": {"atk":133,"deploy":35,"die":29,"dmg":75,"fx":36,"leak":66,"skill":58,"spawn":81,"status":50},
|
||||
"fx": {"aoe":5,"buff":12,"dodge":4,"shield":12,"stealth":3},
|
||||
"snaps": "4724896a 66459592 7ac2242d 02c447fd d2156fd6 fe8971c3 d31ad07d 4308099a 10ecad98 f24a2bb2 554c474f 8dfa861c 410d805c 38503e7a 026cc9f0 78497832 fbf7a44e 14ee15f5 b6054fc0 3d96e8ef 65a507d5 f44850bc 97cf4926"
|
||||
},
|
||||
"diy-099": {
|
||||
"about": "5_diy1_b=crosly/S1/002_crosly 5_diy2_b=crosly/S2/002_crosly 5_diy1_b=crosly/S3/002_crosly 5_diy2_b=crosly/S1/003_crosly 5_diy1_b=crosly/S2/003_crosly 5_diy2_b=crosly/S3/003_crosly 6_diy1_a=crosly/S1 6_diy2_a=crosly/S2 6_diy1_a=crosly/S3 6_diy2_b=crosly/S1 6_diy1_b=crosly/S2 6_diy2_b=crosly/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m04",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"seed": 213318073,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 13,
|
||||
"total": 129,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 118,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":13,"total":129,"perfect":false,"leaks":116,"leaksUncounted":0,"coins":0,"dmg":187799,"bossDmg":0,"heal":0,"deaths":30,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_crosly",966.17,1,162,2,19067,7329,0,102,5],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_crosly",1659,1,163,1,61381,6636,0,49,5],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_crosly",0,0,140,1,11498,4977,0,67,3],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_crosly",0,0,160,2,18928,6612,0,94,4],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_crosly",0,0,161,1,10264,3306,0,48,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_crosly",0,0,50,1,16448,3306,0,45,2],
|
||||
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,45,1,2612,2644,0,23,2],
|
||||
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,55,0,9105,3966,0,12,3],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,53,2,16120,2644,0,63,2],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,4,1,1998,1569,0,15,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,8,1,3821,1569,0,40,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,58,0,16557,3138,0,54,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_10124_uashld_2":[18,0,18,13319,11665],"enemy_1043_zomsbr":[66,13,53,930,145734],"enemy_1061_zomshd":[45,0,43,33447,70081]},
|
||||
"hooks": [1,163,6900,839,839,1910,1897,29104,43,157,32,31,0,0,54,44,129,114,1,43,0,0,13],
|
||||
"events": {"atk":839,"deploy":32,"die":43,"dmg":1897,"fx":109,"leak":114,"skill":63,"spawn":143,"status":100},
|
||||
"fx": {"aoe":9,"dodge":37,"stealth":9,"strike":54},
|
||||
"snaps": "1006c257 0f678064 a704701f 760dad49 431757c0 045bfddf 4870b254 6d60b2a2 b1d2dfcc 5c538fad 7bdc0794 4aa57fe6 157328a0 f8e22f4b 2de427d6 7507374b 0f9b340f 266aa729 37a02fdd 78841a4f 2d2f0704 7fb02184 3af2ffcb"
|
||||
},
|
||||
"diy-100": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 6_diy1_b=crosly/S1/002_crosly 6_diy2_b=crosly/S2/002_crosly 6_diy1_b=crosly/S3/002_crosly 6_diy2_b=crosly/S1/003_crosly 6_diy1_b=crosly/S2/003_crosly 6_diy2_b=crosly/S3/003_crosly 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe",
|
||||
"fast": true,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m01",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 13,
|
||||
"seed": 1853641125,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 219.67,
|
||||
"ticks": 6590,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 171,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 36,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":171,"perfect":false,"leaks":171,"leaksUncounted":0,"coins":0,"dmg":72448,"bossDmg":0,"heal":0,"deaths":30,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,7,0,1302,2128,0,7,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,11,0,2012,2908,0,9,0],
|
||||
["p1",3,"op","chess_char_6_diy1_b",0,"uniequip_002_crosly",1699,1,205,0,15384,11893,0,21,8],
|
||||
["p1",4,"op","chess_char_6_diy2_b",1,"uniequip_002_crosly",0,0,188,0,5292,8495,0,12,5],
|
||||
["p1",5,"op","chess_char_6_diy1_b",2,"uniequip_002_crosly",0,0,53,0,13612,3398,0,11,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",0,"uniequip_003_crosly",0,0,71,0,4916,3408,0,7,2],
|
||||
["p1",7,"op","chess_char_6_diy1_b",1,"uniequip_003_crosly",0,0,61,0,0,3408,0,0,2],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_003_crosly",0,0,64,0,12551,3408,0,6,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,10,0,0,2564,0,0,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,15,0,827,3334,0,1,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,202,0,6335,3813,0,21,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,204,0,10217,4530,0,21,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1009_lurker":[66,0,66,4840,30716],"enemy_1019_jshoot_2":[57,0,57,22944,8793],"enemy_1404_msnip":[48,0,48,25502,32940]},
|
||||
"hooks": [1,205,6590,256,256,302,287,0,30,201,23,22,0,4506,29,24,171,171,1,30,0,0,15],
|
||||
"events": {"atk":256,"deploy":31,"die":30,"dmg":287,"fx":33,"leak":171,"skill":45,"spawn":186,"status":48},
|
||||
"fx": {"aoe":2,"dodge":18,"stealth":4,"strike":9},
|
||||
"snaps": "49f2ffc3 865f68b3 ed4f6cb1 5d7f94f0 0ab325f7 aef88d7b fe215943 0eccc0c9 f8016dfe 52bfa697 959fbc4d 92f3cafe d842aab1 5b84e9b2 21fc74d9 3d5350c3 ad3435c0 c34bce2d 54b81e2d f73960df b3f7f79c 6ffb3cf7"
|
||||
},
|
||||
"diy-101": {
|
||||
"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_m02",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"seed": 739544218,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 27,
|
||||
"total": 99,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 40,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":27,"total":99,"perfect":false,"leaks":72,"leaksUncounted":0,"coins":0,"dmg":313014,"bossDmg":0,"heal":0,"deaths":11,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,318.2,1,78,2,18473,4890,0,110,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,1,0,9712,3371,0,63,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,108,0,8999,3058,0,28,3],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,7,8,89607,1904,0,40,6],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,118,2,22298,2638,0,29,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",3411,1,117,11,66285,3411,0,49,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,5,4,31278,0,0,95,3],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,8,0,50343,0,0,158,3],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,6,0,0,0,0,0,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,9,0,0,0,0,0,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,112,0,6320,1864,0,28,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,119,0,9698,2262,0,28,2]
|
||||
],
|
||||
"enemies": {"enemy_1010_demon":[42,14,27,11195,141094],"enemy_1320_wdrrl_2":[12,2,10,9476,79779],"enemy_1422_lrsldr_2":[45,11,32,2727,92141]},
|
||||
"hooks": [1,119,6900,872,872,1180,1168,0,38,107,24,24,0,31060,56,36,99,69,1,38,0,0,12],
|
||||
"events": {"atk":872,"deploy":20,"die":38,"dmg":1168,"fx":171,"leak":69,"skill":48,"spawn":111,"status":128},
|
||||
"fx": {"aoe":68,"dodge":12,"dp":17,"explode":4,"slash":70},
|
||||
"snaps": "a74f82de faf6851e daae77ac 33d8b813 730e6652 46f27df8 e020739e 700d152f a0154d2d ef7ef2cb 8edea7db 05953255 d5aa0f5d ed9f778e d739fbea d35f22fc f9c2be2a 71687c94 7086e97e daf15f46 515d7e0e 664a79c7 bebedb97"
|
||||
},
|
||||
"diy-102": {
|
||||
"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_m03",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 12,
|
||||
"seed": 2379881591,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 49,
|
||||
"total": 117,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 871,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":49,"total":117,"perfect":false,"leaks":68,"leaksUncounted":0,"coins":0,"dmg":803794,"bossDmg":0,"heal":106219,"deaths":9,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,102,1,9595,3730,0,59,2],
|
||||
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,96,9,92239,14406,0,66,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,442.68,1,132,0,9519,31754,0,154,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",0,0,11,2,15824,38712,0,224,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,8,2,7844,0,0,99,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,12,6,27125,0,0,117,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,14,1,66388,0,0,47,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,4,26,432809,0,0,85,3],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,6,0,0,0,48622,75,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1372.06,1,9,0,0,32913,57597,70,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,13,1,71094,0,0,158,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,15,1,71356,0,0,131,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_1006_shield_3":[18,0,17,43686,299080],"enemy_1043_zomsbr":[72,40,32,208,221745],"enemy_1402_tgshd_2":[27,9,18,77620,282968]},
|
||||
"hooks": [1,138,6900,1517,1517,2075,2182,16473,58,125,23,26,0,35952,7769,31,117,67,1,58,0,0,0],
|
||||
"events": {"atk":1724,"deploy":18,"die":58,"dmg":2164,"fx":951,"heal":243,"leak":67,"skill":56,"spawn":132,"status":1034},
|
||||
"fx": {"aoe":11,"displace":2,"dodge":16,"expose":910,"spGain":12},
|
||||
"snaps": "c6a50909 5be92fc1 640e9a50 ca35b7d8 2fc9f373 46733cc7 5ae803b9 227e5355 19852595 8f005352 8c5b8a89 3c23a34e 785933b8 5769dbab 7dae22b0 8a2bfac3 54fdb303 320ff555 2e4dc6df bea6efc3 536b03e9 dc849abe 4f7418f3"
|
||||
},
|
||||
"diy-103": {
|
||||
"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_m04",
|
||||
"modeId": "mode_multi_normal",
|
||||
"round": 13,
|
||||
"seed": 1753717681,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 208.93,
|
||||
"ticks": 6268,
|
||||
"reason": "cleared",
|
||||
"killed": 35,
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 274,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":35,"total":123,"perfect":false,"leaks":88,"leaksUncounted":0,"coins":0,"dmg":641938,"bossDmg":0,"heal":22385,"deaths":19,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,97,0,2421,5208,0,17,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,6,1,6239,3431,0,17,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1529,1,151,2,14625,3058,0,7,2],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1825.93,1,150,9,117790,6562,0,31,5],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,89,4,24440,7487,0,23,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,14,1,92458,7501,0,42,2],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,133,0,13798,1272,0,22,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,137,7,116590,1643,0,101,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,126,2,162087,1372,0,49,2],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,82,5,91491,4046,0,19,2],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,148,0,0,4772,7983,19,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,106,0,0,13724,14402,22,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_1148_dssbr":[51,16,35,6147,225937],"enemy_1148_dssbr_2":[60,19,41,23919,326677],"enemy_1439_dslntf":[12,0,12,22459,171945]},
|
||||
"hooks": [1,151,6268,531,531,2329,2951,67,54,142,19,18,0,19998,20,64,123,88,1,55,0,20,14],
|
||||
"events": {"atk":615,"deploy":25,"die":54,"dmg":2951,"fx":127,"heal":58,"leak":88,"skill":37,"spawn":138,"status":105},
|
||||
"fx": {"aoe":27,"burst":20,"dodge":14,"dp":18,"slash":47,"spGain":1},
|
||||
"snaps": "0c718e19 446a583b 6aeae38d 08c49a62 f16f19bd 9e422285 82d7dd3c 044f3613 2e719256 35834db2 ed4a4bf3 408824b0 21ee3bd6 2183a198 bae1694b 64c0d658 9fc87778 7258dc1b 99e071a8 264fa6b9 9ea98043"
|
||||
},
|
||||
"diy-104": {
|
||||
"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_m05",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 13,
|
||||
"seed": 3393927053,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 217.67,
|
||||
"ticks": 6530,
|
||||
"reason": "cleared",
|
||||
"killed": 7,
|
||||
"total": 147,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 140,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":7,"total":147,"perfect":false,"leaks":140,"leaksUncounted":0,"coins":0,"dmg":273670,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,167,0,4702,5595,0,12,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,94,0,21769,4988,0,16,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,141.15,1,171,3,63650,10245,0,69,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",924.77,1,172,4,101150,12431,0,71,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,5,0,33739,0,0,112,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,6,0,48660,0,0,124,2]
|
||||
],
|
||||
"enemies": {"enemy_1010_demon_2":[36,0,36,14881,101634],"enemy_1071_dftman":[51,7,44,5724,160137],"enemy_1072_dlancer":[60,0,60,12654,103163]},
|
||||
"hooks": [1,172,6530,450,450,2891,2910,0,28,168,5,21,0,10400,5907,31,147,140,1,28,0,0,0],
|
||||
"events": {"atk":453,"deploy":25,"die":28,"dmg":2805,"fx":55,"leak":140,"skill":41,"spawn":153,"status":890},
|
||||
"fx": {"displace":55},
|
||||
"snaps": "e8fa2837 69bea824 3bfbe1a5 21f25b01 9163092c e0c59785 b715d09f 6e1cbf2b 8b7b01d9 6dd94e3f 2be217e4 246a4dd0 b616283c e0b57ff6 c17df337 827e7ba6 14cce838 f4c4859b 6b2a5076 85880d36 06ebb72c c3a228ef"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user