feedback5: merge O3 — 自选 kits for 逻各斯, 真言 and 维伊; engine hook palsyTrigger (麻痹 interrupting an enemy attack); golden: only diy moved
This commit is contained in:
commit
8357149afa
11 files changed
+2270
-121
No files matched your search
@@ -0,0 +1,390 @@
|
||||
// test/content/op_logos.test.js — the 自选 operator kit of 逻各斯 (char_4133_logos, 6★ 中坚术师; kit
|
||||
// server/sim/content/kits/ops/op-logos.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, CCR-Δ
|
||||
// “来自河谷的笔盒” or CCR-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_logos.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 LOGOS = 'char_4133_logos';
|
||||
const FORMS = BACKUPS.units[LOGOS].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const CCRD = 'uniequip_002_logos', CCRY = 'uniequip_003_logos';
|
||||
const S1 = 'skchr_logos_1', S2 = 'skchr_logos_2', S3 = 'skchr_logos_3';
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
/** The talent bb of a form with a module's changes (stage 2+ rewrites 语汇演化). */
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const m = elite ? modOf(tier, mod) : null;
|
||||
return m?.talentChanges.find((t) => t.talentIndex === i)?.bb ?? formOf(tier, elite).talents.find((t) => t.index === i).bb;
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = {
|
||||
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_elite: dummy('enemy_elite', { rank: 'ELITE' }),
|
||||
enemy_boss: dummy('enemy_boss', { rank: 'BOSS' }), enemy_low: dummy('enemy_low', { hp: 300 }),
|
||||
enemy_gun: dummy('enemy_gun', { atk: 100, range: 3, bat: 1, applyWay: 'RANGED' }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, CCRD, CCRY].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. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5, setup } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed, setup,
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'attack', 'elementBurst'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: LOGOS, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
|
||||
const tagged = (h, u, tag) => h.hooksOf('damaged').filter((c) => c.source === u && (c.dmg?.tags || []).includes(tag));
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Start the skill now (SP filled, no enemy needed). */
|
||||
function cast(u) { u.skill.gainSp(999); assert.ok(u.skill.activate('test')); }
|
||||
|
||||
test('逻各斯 in every 自选 form: his operator kit (all three skills authored), stats + module attributes, 3-1 range, ranged arts that hits air units, targetable by ground enemies, 空 bond, no 特质', () => {
|
||||
assert.equal(OPERATOR_KITS[LOGOS], KITS[LOGOS]);
|
||||
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], [LOGOS, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.res], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, form.stats.res + (m?.attr.res ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 中坚术师`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// the numbers of the forms (zh_CN): E2 Lv1 1297 / 556, E2 Lv60 1540 / 632; CCR-Δ +100 / +36 → +170 / +67 HP / ATK,
|
||||
// CCR-Y +35 / +5 → +60 / +5 ATK / RES
|
||||
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], [1297, 556, 1540, 632]);
|
||||
assert.deepEqual([modOf(5, CCRD).attr, modOf(6, CCRD).attr, modOf(5, CCRY).attr, modOf(6, CCRY).attr], [{ maxHp: 100, atk: 36 }, { maxHp: 170, atk: 67 }, { atk: 35, res: 5 }, { atk: 60, res: 5 }]);
|
||||
});
|
||||
|
||||
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(LOGOS));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(LOGOS), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: LOGOS, skillIndex: 2, uniEquipId: CCRY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: LOGOS, skillIndex: 2, uniEquipId: CCRY } } });
|
||||
});
|
||||
|
||||
test('S1 殁亡 (AUTO, data DEFAULT): 74 / 68 SP from 0, cast as he is about to attack an enemy of his 3-1; then for good the 3-3 range and ATK +40 % / +70 %', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
const { h, u } = field({ tier, elite, skill: 0 });
|
||||
assert.deepEqual([u.skill.rule, u.skill.kind, u.skill.spCost, sk.initSp, sk.bb.atk, sk.rangeId], ['DEFAULT', 'toggle', elite ? 68 : 74, 0, elite ? 0.7 : 0.4, '3-3'], `T${tier}`);
|
||||
h.run(sk.spCost - 1);
|
||||
assert.equal(u.skill.activations, 0, `T${tier}: not before its SP is full`);
|
||||
h.run(3);
|
||||
assert.equal(u.skill.activations, 0, `T${tier}: full SP, no enemy: no cast`);
|
||||
h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in his range`);
|
||||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-3 while on`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||||
h.run(200);
|
||||
assert.ok(u.skill.active, `T${tier}: 持续时间无限`);
|
||||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: still 3-3`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 殁亡 execution: an enemy of his range under 90 % / 120 % of his ATK falls at once (9999999 无来源 true damage, no dodge), then another random enemy of the range takes arts damage equal to the HP it had left; one try only on a survivor; air units too', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
let guard = null;
|
||||
const { h, u } = field({ tier, elite, skill: 0, setup: (b) => b.on('fatal', (c) => { if (c.unit === guard) c.prevented = true; }) });
|
||||
cast(u);
|
||||
const thr = sk.bb['attack@kill_atk_scale'] * u.s.atk;
|
||||
assert.equal(sk.bb['attack@kill_damage'], 9999999);
|
||||
const big = h.spawn('enemy_dummy', { pos: [11, 8] }); // a 3-3 tile outside the 3-1 (the toggle's range counts)
|
||||
const low = h.spawn('enemy_fly', { pos: [10, 7] });
|
||||
const hp0 = thr * 0.6;
|
||||
low.hp = hp0;
|
||||
const high = h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
h.step();
|
||||
assert.ok(!low.alive, `T${tier}: the flyer under the threshold fell`);
|
||||
const kill = h.hooksOf('damaged').find((c) => c.target === low && (c.dmg?.tags || []).includes('execute'));
|
||||
assert.ok(kill && kill.source === null && kill.credit === u && kill.type === 'true' && kill.dmg.sourceless && !kill.dmg.canDodge, `T${tier}: 无来源 true, credited to him`);
|
||||
assert.equal(low.removeReason, 'killed');
|
||||
const follow = tagged(h, u, 'logosPerish');
|
||||
assert.equal(follow.length, 1, `T${tier}: one follow-up`);
|
||||
assert.ok([big, high].includes(follow[0].target) && follow[0].type === 'arts', `T${tier}: on another enemy of his range`);
|
||||
approx(follow[0].amount, hp0, `T${tier}: the HP the fallen one had left`);
|
||||
assert.ok(![big, high].some((e) => h.hooksOf('damaged').some((c) => c.target === e && (c.dmg?.tags || []).includes('execute'))), `T${tier}: above the threshold: no execution`);
|
||||
// an enemy that falls under the threshold while on his range is executed then (the aura checks every tick)
|
||||
high.hp = thr * 0.9;
|
||||
h.step();
|
||||
assert.ok(!high.alive, `T${tier}: executed once under the threshold`);
|
||||
// a survivor (不死) is never tried again
|
||||
guard = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
guard.hp = thr * 0.5;
|
||||
h.step();
|
||||
const tries = () => h.hooksOf('damaged').filter((c) => c.target === guard && (c.dmg?.tags || []).includes('execute')).length;
|
||||
assert.equal(tries(), 1, `T${tier}: tried once`);
|
||||
assert.ok(guard.alive && guard.hp <= 1 + 1e-9, `T${tier}: held at 1 HP`);
|
||||
h.run(3);
|
||||
assert.equal(tries(), 1, `T${tier}: never again`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 提喻 (MANUAL, data DEFAULT): 20 s, RES +40 / +50; locks one target with a hit every 0.5 s whatever the attack speed, 35 %→105 % / 50 %→150 % of ATK and its speed down to 40 % over 10 hits; a stun breaks the lock', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
const b = sk.bb;
|
||||
const { h, u } = field({ tier, elite, skill: 1, seed: 7 });
|
||||
assert.deepEqual([u.skill.rule, u.skill.kind, sk.duration, b.magic_resistance, b['attack@cooldown'], b['attack@atk_scale_base'], b['attack@atk_scale_delta'], b['attack@move_speed'], b['attack@max_stack_cnt']],
|
||||
['DEFAULT', 'duration', 20, elite ? 50 : 40, 0.5, elite ? 0.5 : 0.35, elite ? 0.1 : 0.07, -0.06, 10], `T${tier}`);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.b.addBuff(u, { key: 'test:aspd', mods: { aspd: 300 } }); // the lock ignores the attack speed
|
||||
const res0 = u.s.res;
|
||||
cast(u);
|
||||
approx(u.s.res, res0 + b.magic_resistance, `T${tier}: RES`);
|
||||
const n0 = atkHits(h, u).length;
|
||||
h.run(6.02);
|
||||
const hits = atkHits(h, u).slice(n0);
|
||||
assert.ok(hits.length >= 12 && hits.length <= 13, `T${tier}: ${hits.length} hits in 6 s (one per 0.5 s)`);
|
||||
for (let i = 1; i < hits.length; i++) approx(hits[i].t - hits[i - 1].t, 0.5, `T${tier}: cadence`, 0.07);
|
||||
hits.forEach((c, i) => {
|
||||
assert.equal(c.target, e, `T${tier}: the locked target`);
|
||||
// + 剜魂具辞's 150 (the first hit marks it already); the target's RES 0
|
||||
approx(c.amount, u.s.atk * (b['attack@atk_scale_base'] + b['attack@atk_scale_delta'] * Math.min(10, i)) + 150, `T${tier}: hit ${i}`, 1e-4);
|
||||
});
|
||||
assert.ok(Math.abs(hits[10].amount - (u.s.atk * b['attack@atk_scale_base'] * 3 + 150)) < 1e-3 * hits[10].amount, `T${tier}: 3× after 5 s`);
|
||||
approx(e.findBuff(`logos:lockSlow:${u.id}`).mods.moveMul, 0.4, `T${tier}: 40 % speed`);
|
||||
// a stun breaks it: the slow goes, the next hits start over
|
||||
h.b.applyStatus(u, 'stun', { duration: 1, source: e });
|
||||
h.step();
|
||||
assert.equal(e.findBuff(`logos:lockSlow:${u.id}`), null, `T${tier}: slow gone with the lock`);
|
||||
const n1 = atkHits(h, u).length;
|
||||
h.run(1.6);
|
||||
const after = atkHits(h, u).slice(n1);
|
||||
approx(after[0].amount, u.s.atk * b['attack@atk_scale_base'] + 150, `T${tier}: reset`, 1e-4);
|
||||
// beam: the hit lands as the attack is made (no bolt in flight)
|
||||
const atks = h.hooksOf('attack').filter((c) => c.attacker === u);
|
||||
assert.ok(Math.abs(atks[atks.length - 1].t - after[after.length - 1].t) < 1e-9, `T${tier}: instant`);
|
||||
h.runUntil(() => !u.skill.active, 25);
|
||||
approx(u.s.res, res0, `T${tier}: RES back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 提喻 keeps its lock while another enemy comes first in the order, and locks anew when its target leaves the field', () => {
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 1 });
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
cast(u);
|
||||
h.run(1.1);
|
||||
assert.ok(atkHits(h, u).every((c) => c.target === a));
|
||||
h.spawn('enemy_dummy', { pos: [10, 6], routeIndex: 0 }); // nearer the goal: first in the order
|
||||
const n0 = atkHits(h, u).length;
|
||||
h.run(1.6);
|
||||
assert.ok(atkHits(h, u).slice(n0).every((c) => c.target === a), 'still the locked one');
|
||||
h.b.kill(a, null);
|
||||
const n1 = atkHits(h, u).length;
|
||||
h.run(1.1);
|
||||
const next = atkHits(h, u).slice(n1);
|
||||
assert.ok(next.length >= 2 && next.every((c) => c.target !== a));
|
||||
approx(next[0].amount, u.s.atk * 0.5 + 150, 'a new lock starts at the base scale', 1e-4);
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 延异视阈 (MANUAL, data ACTIVE_RANGE on its 3-4): cast with an enemy on the 3-4 only, 30 s, ATK +160 % / +220 %, 3 targets at once', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const { h, u } = field({ tier, elite, skill: 2 });
|
||||
assert.deepEqual([u.skill.rule, sk.rangeId, sk.duration, sk.bb.atk, sk.bb['attack@max_target'], sk.bb.projectile_move_scale], ['ACTIVE_RANGE', '3-4', 30, elite ? 2.2 : 1.6, 3, 0.05], `T${tier}`);
|
||||
assert.deepEqual(u.skill.triggerGrid, sk.rangeGrid, `T${tier}: the trigger grid is the 3-4`);
|
||||
u.skill.gainSp(999);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, `T${tier}: no enemy`);
|
||||
const far = h.spawn('enemy_dummy', { pos: [12, 6] }); // dRow +2: the 3-4 only
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast for an enemy outside his own range`);
|
||||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-4 while on`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||||
const more = [h.spawn('enemy_dummy', { pos: [10, 6] }), h.spawn('enemy_fly', { pos: [9, 7] }), h.spawn('enemy_dummy', { pos: [11, 8] })];
|
||||
const a0 = h.hooksOf('attack').filter((c) => c.attacker === u).length;
|
||||
h.run(5);
|
||||
const atks = h.hooksOf('attack').filter((c) => c.attacker === u).slice(a0);
|
||||
assert.ok(atks.length >= 2 && atks.every((c) => c.targets.length === 3), `T${tier}: 3 targets per attack`);
|
||||
assert.ok(atks.every((c) => c.targets.every((e) => [far, ...more].includes(e))));
|
||||
h.runUntil(() => !u.skill.active, 40);
|
||||
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-1`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 延异视阈: enemy shots on his range fly at 5 % of their speed (back once they leave it) and are cleared when the skill ends — not when he is disarmed then', () => {
|
||||
for (const disarm of [false, true]) {
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2 });
|
||||
h.spawn('enemy_gun', { pos: [10, 7] });
|
||||
h.run(1.5);
|
||||
cast(u);
|
||||
h.run(0.2);
|
||||
h.run(3);
|
||||
const shots = h.b.projectiles.list.filter((p) => p.source && p.source.side === 'enemy');
|
||||
assert.ok(shots.length >= 2, `${shots.length} enemy shots held`);
|
||||
for (const p of shots) approx(p.speed, 10 * 0.05, 'slowed to 0.05×');
|
||||
if (disarm) h.b.applyStatus(u, 'disarm', { duration: 40, source: null });
|
||||
h.runUntil(() => u.skill.timeLeft < 0.5, 40);
|
||||
for (const g of h.enemies()) h.b.applyStatus(g, 'stun', { duration: 99, source: null }); // no new shot
|
||||
const held = h.b.projectiles.list.filter((p) => p.source && p.source.side === 'enemy').length;
|
||||
assert.ok(held >= 1, `${held} shots in the air as it ends`);
|
||||
const taken = u.stats.taken;
|
||||
h.runUntil(() => !u.skill.active, 2);
|
||||
h.step();
|
||||
const left = h.b.projectiles.list.filter((p) => p.source && p.source.side === 'enemy');
|
||||
if (!disarm) {
|
||||
assert.equal(left.length, 0, 'cleared');
|
||||
h.run(5);
|
||||
assert.equal(u.stats.taken, taken, 'none of the held shots hit him');
|
||||
} else {
|
||||
assert.ok(left.length > 0 && left.every((p) => Math.abs(p.speed - 10) < 1e-9), 'disarmed: kept, at full speed again');
|
||||
}
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 语汇演化: 40 % per attacked target for an extra 60 % ATK arts hit on a random enemy of his range + 停顿 0.8 s; CCR-Δ stage 3: 60 % and 60 % ATK 元素伤害 on a target in its 凋亡 burst; CCR-Y stage 3: two executions at 65 %', () => {
|
||||
for (const f of [[5, false, null], [5, true, CCRD], [6, true, CCRD], [6, true, CCRY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t = talentOf(tier, elite, mod, 0);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 21 });
|
||||
h.b.addBuff(u, { key: 'test:aspd', mods: { aspd: 500 } });
|
||||
const es = [h.spawn('enemy_dummy', { pos: [10, 7] }), h.spawn('enemy_fly', { pos: [11, 6] })];
|
||||
h.run(50);
|
||||
const attacks = h.hooksOf('attack').filter((c) => c.attacker === u).reduce((n, c) => n + c.targets.length, 0);
|
||||
const extra = tagged(h, u, 'logosLexicon').filter((c) => c.type === 'arts');
|
||||
const emit = t.emit_count ?? 1;
|
||||
const ratio = extra.length / (attacks * emit);
|
||||
assert.ok(attacks > 150 && Math.abs(ratio - t.prob) < 0.08, `${label(f)}: ${extra.length} / ${attacks}×${emit} ≈ ${t.prob}`);
|
||||
for (const c of extra.slice(0, 6)) {
|
||||
assert.ok(es.includes(c.target), `${label(f)}: an enemy of his range (air too)`);
|
||||
const marked = h.hooksOf('damaged').some((d) => d.source === u && d.target === c.target && d.dmg?.isAttack && d.t <= c.t && c.t - d.t <= 5);
|
||||
approx(c.amount, u.s.atk * t.atk_scale + (marked ? 150 : 0), `${label(f)}: ${t.atk_scale * 100} % ATK (+ 剜魂具辞 on a marked one)`, 1e-4);
|
||||
}
|
||||
const slug = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'sluggish');
|
||||
assert.ok(slug.length >= extra.length * 0.95 && slug.every((c) => Math.abs(c.duration - t.sluggish) < 1e-9), `${label(f)}: 停顿 ${t.sluggish} s`);
|
||||
const elem = tagged(h, u, 'logosLexicon').filter((c) => c.type === 'elemental');
|
||||
if (t.element_atk_scale) {
|
||||
assert.ok(elem.length > 0, `${label(f)}: 元素伤害 during the 凋亡 bursts (his own CCR-Δ fills them)`);
|
||||
for (const c of elem) {
|
||||
approx(c.amount, u.s.atk * t.element_atk_scale, `${label(f)}: ${t.element_atk_scale * 100} % ATK 元素伤害`, 1e-4);
|
||||
assert.ok(h.hooksOf('elementBurst').some((b) => b.target === c.target && b.element === 'apoptosis' && b.t <= c.t && c.t - b.t < 15), 'in its 凋亡 burst');
|
||||
}
|
||||
} else assert.equal(elem.length, 0, `${label(f)}: no 元素伤害`);
|
||||
done(h);
|
||||
}
|
||||
// the module texts
|
||||
assert.match(modOf(6, CCRD).talentChanges.find((t) => t.talentIndex === 0).desc, /60%几率.*凋亡损伤爆发期间则同时造成相当于攻击力60%的元素伤害/);
|
||||
assert.match(modOf(6, CCRY).talentChanges.find((t) => t.talentIndex === 0).desc, /两个随机目标造成相当于攻击力65%/);
|
||||
assert.equal(modOf(5, CCRD).talentChanges.find((t) => t.talentIndex === 0), undefined, 'stage 1: the talent unchanged');
|
||||
});
|
||||
|
||||
test('T1 语汇演化 also follows 殁亡\'s follow-up hit (备注 "可触发第一天赋") and works while he is disarmed', () => {
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 0, seed: 3 });
|
||||
cast(u);
|
||||
h.b.applyStatus(u, 'disarm', { duration: 999, source: null });
|
||||
const thr = 1.2 * u.s.atk;
|
||||
let rolls = 0;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
h.spawn('enemy_dummy', { pos: [9, 7] });
|
||||
const low = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
low.hp = thr * 0.5;
|
||||
h.step();
|
||||
rolls++;
|
||||
for (const e of h.enemies()) h.b.kill(e, null);
|
||||
h.step();
|
||||
}
|
||||
assert.equal(atkHits(h, u).length, 0, 'disarmed: no attack');
|
||||
assert.equal(tagged(h, u, 'logosPerish').length, rolls);
|
||||
const extra = tagged(h, u, 'logosLexicon');
|
||||
assert.ok(extra.length > rolls * 0.2 && extra.length < rolls * 0.6, `${extra.length} / ${rolls} ≈ 40 %`);
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('T2 剜魂具辞: each of his attack hits gives the target RES −10 and +150 on every arts damage it takes for 5 s — the hit itself included, other casters\' arts too; one instance per enemy', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, CCRY]]) {
|
||||
const t = talentOf(tier, elite, mod, 1);
|
||||
assert.deepEqual(t, { duration: 5, magic_resistance: -10, atk_addition: 150 });
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 9 });
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
e.base.res = 30; e.markDirty();
|
||||
const first = () => atkHits(h, u)[0];
|
||||
assert.ok(h.runUntil(() => !!first(), 4));
|
||||
const c = first();
|
||||
approx(c.amount, (u.s.atk + 150) * (1 - 0.2), `T${tier}: (ATK + 150) × (1 − (30 − 10) %) on the very hit`, 1e-4);
|
||||
const mark = e.findBuff('logos:soulRend');
|
||||
assert.ok(mark && Math.abs(mark.mods.resFlat + 10) < 1e-9 && mark.data.value === 150);
|
||||
approx(e.s.res, 20, `T${tier}: RES −10`);
|
||||
// another source's arts damage gets the 150 too; physical does not
|
||||
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'arts', canDodge: false }), (1000 + 150) * 0.8, 'arts +150');
|
||||
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'phys', canDodge: false }), 1000, 'phys unchanged');
|
||||
// it lapses 5 s after his last hit
|
||||
h.b.applyStatus(u, 'stun', { duration: 20, source: null });
|
||||
h.run(5.2);
|
||||
assert.equal(e.findBuff('logos:soulRend'), null, `T${tier}: gone after 5 s`);
|
||||
approx(e.s.res, 30, `T${tier}: RES back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('CCR-Δ “来自河谷的笔盒” (stages 1 and 3): 8 % of every arts damage he deals as 凋亡 损伤; none without it', () => {
|
||||
for (const f of [[5, false, null], [5, true, null], [5, true, CCRD], [6, true, CCRD], [6, true, CCRY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 4 });
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.run(4);
|
||||
const arts = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.type === 'arts');
|
||||
const fills = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.type === 'element');
|
||||
assert.ok(arts.length > 0);
|
||||
if (elite && mod === CCRD) {
|
||||
assert.equal(u.def.raw.talents.find((t) => t.index === -1).bb.ep_damage_ratio, 0.08);
|
||||
assert.equal(fills.length, arts.length, `${label(f)}: one fill per arts hit`);
|
||||
fills.forEach((c, i) => {
|
||||
assert.equal(c.dmg.element, 'apoptosis');
|
||||
approx(c.amount, arts[i].amount * 0.08, `${label(f)}: 8 %`);
|
||||
});
|
||||
} else assert.equal(fills.length, 0, `${label(f)}: no 凋亡`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('CCR-Y “《语义范式百科》” (stages 1 and 3): +1 SP per normal-attack hit on an elite or leader enemy (none on a normal one, none while a skill runs); none without it', () => {
|
||||
for (const f of [[5, true, CCRY], [6, true, CCRY], [6, true, CCRD], [6, false, null]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const y = elite && mod === CCRY;
|
||||
if (y) assert.deepEqual(modOf(tier, CCRY).traitOverride.bb, { sp: 1 });
|
||||
for (const key of ['enemy_dummy', 'enemy_elite', 'enemy_boss']) {
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 2 });
|
||||
const e = h.spawn(key, { pos: [10, 7] });
|
||||
h.b.applyStatus(u, 'disarm', { duration: 3, source: null });
|
||||
h.run(3.1);
|
||||
const sp0 = u.skill.sp, t0 = h.b.time;
|
||||
h.run(5);
|
||||
const hits = [...atkHits(h, u), ...tagged(h, u, 'logosLexicon')].filter((c) => c.target === e && c.t >= t0 - 1e-9).length;
|
||||
const gain = u.skill.sp - sp0 - 5; // 5 s of natural SP
|
||||
const want = y && key !== 'enemy_dummy' ? hits : 0;
|
||||
approx(gain, want, `${label(f)} ${key}: ${hits} hits`, 1e-6);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,371 @@
|
||||
// test/content/op_mantra.test.js — the 自选 operator kit of 真言 (char_4204_mantra, 6★ 本源术师; kit
|
||||
// server/sim/content/kits/ops/op-mantra.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
|
||||
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or PRI-X
|
||||
// “中枢神经探测模块” 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_mantra.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 MANTRA = 'char_4204_mantra';
|
||||
const FORMS = BACKUPS.units[MANTRA].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const PRIX = 'uniequip_002_mantra';
|
||||
const S1 = 'skchr_mantra_1', S2 = 'skchr_mantra_2', S3 = 'skchr_mantra_3';
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const m = elite ? modOf(tier, mod) : null;
|
||||
return m?.talentChanges.find((t) => t.talentIndex === i)?.bb ?? formOf(tier, elite).talents.find((t) => t.index === i).bb;
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = {
|
||||
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
|
||||
enemy_shooter: dummy('enemy_shooter', { atk: 1, bat: 1, range: 4 }),
|
||||
enemy_walker: enemyRec({ key: 'enemy_walker', hp: 1e9, speed: 0.3, mass: 0 }),
|
||||
enemy_immune: dummy('enemy_immune', { otherImmunities: ['palsy'] }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, PRIX].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5, setup, stage } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed, setup, ...(stage ? { flat: stage } : null),
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'attack', 'elementBurst'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: MANTRA, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
|
||||
const tagged = (h, u, tag, src = u) => h.hooksOf('damaged').filter((c) => (c.source === src || c.credit === src) && (c.dmg?.tags || []).includes(tag));
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
function cast(u) { u.skill.gainSp(999); assert.ok(u.skill.activate('test')); }
|
||||
const palsy = (e) => e.findBuff('palsy')?.stacks ?? 0;
|
||||
|
||||
test('真言 in every 自选 form: her operator kit (all three skills authored), stats + PRI-X attributes, 3-1 range, ranged arts that hits air units, targetable by ground enemies, 萨尔贡 bond, no 特质', () => {
|
||||
assert.equal(OPERATOR_KITS[MANTRA], KITS[MANTRA]);
|
||||
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], [MANTRA, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 本源术师`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['sargonShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
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], [1071, 583, 1308, 657]);
|
||||
assert.deepEqual([modOf(5, PRIX).attr, modOf(6, PRIX).attr], [{ maxHp: 90, atk: 43 }, { maxHp: 145, atk: 87 }]);
|
||||
});
|
||||
|
||||
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(MANTRA));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(MANTRA), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: MANTRA, skillIndex: 2, uniEquipId: PRIX } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[5]]: { charId: MANTRA, skillIndex: 2, uniEquipId: PRIX } } });
|
||||
});
|
||||
|
||||
test('S1 共鸣溃缩 (AUTO, attack SP 3, data DEFAULT): every 4th attack deals 230 % / 275 % ATK arts, then 15 % / 20 % of it as 神经 损伤, then — on a target in its 神经 burst — 150 % / 160 % ATK 元素伤害', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
const { h, u } = field({ tier, elite, skill: 0 });
|
||||
assert.deepEqual([u.skill.rule, u.skill.kind, u.skill.spType, u.skill.spCost, sk.bb.atk_scale, sk.bb.ep_damage_ratio, sk.bb.element_atk_scale],
|
||||
['DEFAULT', 'instant', 'attack', 3, elite ? 2.75 : 2.3, elite ? 0.2 : 0.15, elite ? 1.6 : 1.5], `T${tier}`);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.run(16 * 1.6 + 0.5);
|
||||
const hits = atkHits(h, u).filter((c) => c.target === e);
|
||||
const skillHits = hits.filter((c) => c.dmg.isSkill);
|
||||
assert.ok(hits.length >= 15, `T${tier}: ${hits.length} attacks`);
|
||||
hits.forEach((c, i) => assert.equal(c.dmg.isSkill, i % 4 === 3, `T${tier}: attack ${i + 1}`));
|
||||
const fills = h.hooksOf('damaged').filter((c) => c.source === u && c.type === 'element');
|
||||
for (const c of skillHits) {
|
||||
const fill = fills.find((x) => Math.abs(x.t - c.t) < 1e-9);
|
||||
if (fill) approx(fill.amount, c.amount * sk.bb.ep_damage_ratio, `T${tier}: 神经 = ${sk.bb.ep_damage_ratio * 100} % of the damage`);
|
||||
}
|
||||
approx(skillHits[0].amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`);
|
||||
assert.ok(fills.length >= 1 && fills.every((x) => x.dmg.element === 'neural'));
|
||||
// in its 神经 burst: + element_atk_scale × ATK 元素伤害 after the 神经 fill (a fresh target one point short of it)
|
||||
h.b.kill(e, null);
|
||||
const e2 = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
e2.elem.neural = e2.gaugeMax - 1;
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
h.runUntil(() => h.hooksOf('damaged').slice(n0).some((c) => c.source === u && c.dmg?.isSkill && c.dmg?.isAttack), 8);
|
||||
h.step();
|
||||
const after = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.type === 'elemental' && (c.dmg.tags || []).includes('mantra'));
|
||||
assert.ok(h.hooksOf('elementBurst').some((c) => c.target === e2 && c.element === 'neural'), `T${tier}: her 神经 burst it`);
|
||||
assert.equal(after.length, 1, `T${tier}: the burst made by its own 神经 counts`);
|
||||
approx(after[0].amount, u.s.atk * sk.bb.element_atk_scale, `T${tier}: ${sk.bb.element_atk_scale * 100} % ATK 元素伤害`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 意识联协 (MANUAL, data DEFAULT): 25 s, attack interval 1.6 − 0.6 / 0.7 s; 170 % / 210 % ATK on the target, then 2 / 3 jumps at 3.2 tiles/s to the nearest new enemy within 2.0 tiles for 100 % / 110 %; 神经 riders on every hit', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
const b = sk.bb;
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
assert.deepEqual([u.skill.rule, sk.duration, b.base_attack_time, b['attack@atk_scale'], b['attack@chain.atk_scale'], b.chain_times, b['attack@ep_damage_ratio'], b['attack@element_atk_scale']],
|
||||
['DEFAULT', 25, elite ? -0.7 : -0.6, elite ? 2.1 : 1.7, elite ? 1.1 : 1, elite ? 3 : 2, elite ? 0.15 : 0.13, elite ? 0.2 : 0.18], `T${tier}`);
|
||||
// a line of enemies 1.5 tiles apart (the 4th is 2.1 from the 3rd: out of the jump radius at tier 6)
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
const j1 = h.spawn('enemy_dummy', { pos: [10, 8.5] });
|
||||
const j2 = h.spawn('enemy_fly', { pos: [10, 10] });
|
||||
const j3 = h.spawn('enemy_dummy', { pos: [11.5, 10] });
|
||||
const out = h.spawn('enemy_dummy', { pos: [11.5, 12.1] });
|
||||
cast(u);
|
||||
approx(u.s.interval, 1.6 + b.base_attack_time, `T${tier}: interval`);
|
||||
const a0 = h.hooksOf('attack').filter((c) => c.attacker === u).length;
|
||||
h.runUntil(() => h.hooksOf('attack').filter((c) => c.attacker === u).length > a0, 2);
|
||||
const at = h.hooksOf('attack').filter((c) => c.attacker === u).slice(-1)[0];
|
||||
assert.equal(at.targets[0], main, `T${tier}: the first target`);
|
||||
h.run(3);
|
||||
const id = atkHits(h, u).find((c) => c.target === main && c.t >= at.t).dmg.attackId;
|
||||
const chainHits = atkHits(h, u).filter((c) => c.dmg.attackId === id);
|
||||
const order = chainHits.map((c) => c.target);
|
||||
assert.deepEqual(order, [main, j1, j2, j3].slice(0, 1 + b.chain_times), `T${tier}: the nearest new enemy each time`);
|
||||
assert.ok(!order.includes(out), `T${tier}: 2.1 tiles: out of reach`);
|
||||
approx(chainHits[0].amount, u.s.atk * b['attack@atk_scale'], `T${tier}: main`);
|
||||
for (const c of chainHits.slice(1)) {
|
||||
approx(c.amount, u.s.atk * b['attack@chain.atk_scale'], `T${tier}: jump`);
|
||||
assert.ok(c.dmg.tags.includes('chain') && c.dmg.isSkill);
|
||||
}
|
||||
approx(chainHits[2].t - chainHits[1].t, 1.5 / 3.2, `T${tier}: 3.2 tiles/s`, 0.1);
|
||||
for (const [i, c] of chainHits.entries()) {
|
||||
const fill = h.hooksOf('damaged').find((x) => x.source === u && x.type === 'element' && x.target === c.target && Math.abs(x.t - c.t) < 1e-9);
|
||||
assert.ok(fill, `T${tier}: a 神经 fill with hit ${i}`);
|
||||
approx(fill.amount, c.amount * b['attack@ep_damage_ratio'], `T${tier}: 神经 ${i}`);
|
||||
}
|
||||
h.runUntil(() => !u.skill.active, 30);
|
||||
approx(u.s.interval, 1.6, `T${tier}: interval back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 无言为真 (MANUAL, data ACTIVE_RANGE on its 3-17): cast for an enemy outside her own range, 40 s, ATK +170 % / +210 %, 2 targets, the 3-17 while on; 隐匿 enemies on it are revealed', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const { h, u } = field({ tier, elite, skill: 2 });
|
||||
assert.deepEqual([u.skill.rule, sk.rangeId, sk.duration, sk.bb.atk, sk.bb['attack@max_target'], sk.bb.max_trigger_cnt, sk.bb.per_active, sk.bb.interval_projectile_trigger, sk.bb.atk_scale],
|
||||
['ACTIVE_RANGE', '3-17', 40, elite ? 2.1 : 1.7, 2, 3, 2, 1.5, elite ? 1.45 : 1.1], `T${tier}`);
|
||||
u.skill.gainSp(999);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0);
|
||||
const far = h.spawn('enemy_dummy', { pos: [12, 6] }); // dRow +2: the 3-17 only
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast`);
|
||||
assert.ok(!formOf(tier, elite).rangeGrid.some(([r, c]) => r === far.y - 10 && c === far.x - 5), `T${tier}: the enemy is not on her own 3-1`);
|
||||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-17`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||||
const sneak = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.b.addBuff(sneak, { key: 'test:stealth', flags: { stealth: true } });
|
||||
h.step();
|
||||
assert.ok(sneak.s.flags.reveal, `T${tier}: revealed on her range`);
|
||||
const a0 = h.hooksOf('attack').filter((c) => c.attacker === u).length;
|
||||
h.run(4);
|
||||
const atks = h.hooksOf('attack').filter((c) => c.attacker === u).slice(a0);
|
||||
assert.ok(atks.length >= 2 && atks.every((c) => c.targets.length === 2), `T${tier}: 2 targets`);
|
||||
h.runUntil(() => !u.skill.active, 45);
|
||||
h.run(0.3);
|
||||
assert.ok(!sneak.s.flags.reveal, `T${tier}: hidden again once it ends`);
|
||||
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-1`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 无言为真 麻痹付与 and the cap 2: 0.1 s after the cast every enemy on her range gets 2 麻痹; another operator on her range starting a skill grants again (3 times per cast, a grant kept while nobody is there); stacks over 2 become 溢出麻痹 — each an 元素伤害 bounce of 110 % / 145 % ATK on the holder and the nearest other enemy within 2.0 tiles, one at once and one per 1.5 s', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const others = [{ uid: 2, chessId: 'chess_char_1_08_a', row: 11, col: 6 }, { uid: 3, chessId: 'chess_char_1_08_a', row: 9, col: 2 }];
|
||||
const { h, u } = field({ tier, elite, skill: 2, others });
|
||||
const inside = h.unit(2), outside = h.unit(3);
|
||||
for (const a of [inside, outside]) { a.skill.sp = 0; a.skill.charges = 0; }
|
||||
cast(u);
|
||||
h.run(0.05);
|
||||
// nobody on her range yet: the grant at 0.1 s is kept
|
||||
h.run(0.1);
|
||||
assert.equal(u.mem.mantraS3.grants, 3, `T${tier}: kept`);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
const near = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
h.step();
|
||||
// an operator outside her range starting a skill: nothing
|
||||
outside.skill.gainSp(999); outside.skill.activate('test');
|
||||
h.step();
|
||||
assert.equal(palsy(e), 0, `T${tier}: an operator off her range`);
|
||||
// the one on her range: the first grant (2 stacks, at the cap)
|
||||
inside.skill.gainSp(999); inside.skill.activate('test');
|
||||
h.step();
|
||||
assert.deepEqual([palsy(e), palsy(near), u.mem.mantraS3.grants], [2, 2, 2], `T${tier}: 2 麻痹 each`);
|
||||
assert.equal(tagged(h, u, 'mantraOverflow').length, 0, `T${tier}: no overflow yet`);
|
||||
// the second: 2 over the cap each ⇒ 溢出麻痹 2 each, one bounce at once each
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
inside.skill.gainSp(999); inside.skill.activate('test');
|
||||
h.step();
|
||||
assert.deepEqual([palsy(e), palsy(near)], [2, 2], `T${tier}: capped at 2`);
|
||||
let bounces = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && (c.dmg.tags || []).includes('mantraOverflow'));
|
||||
assert.deepEqual(bounces.map((c) => c.target.id).sort(), [e.id, near.id].sort(), `T${tier}: each holder hit at once`);
|
||||
for (const c of bounces) {
|
||||
approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`);
|
||||
assert.deepEqual([c.type, c.dmg.element], ['elemental', 'neural']);
|
||||
}
|
||||
h.run(0.5); // the jumps land (1 tile at 3.2 tiles/s)
|
||||
bounces = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && (c.dmg.tags || []).includes('mantraOverflow'));
|
||||
assert.equal(bounces.length, 4, `T${tier}: holder + the nearest other, twice`);
|
||||
h.run(1.1); // 1.5 s after: the second 溢出麻痹 of each
|
||||
bounces = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && (c.dmg.tags || []).includes('mantraOverflow'));
|
||||
assert.ok(bounces.length >= 6, `T${tier}: the next one 1.5 s later (${bounces.length})`);
|
||||
// the third grant, then no more
|
||||
inside.skill.gainSp(999); inside.skill.activate('test');
|
||||
h.step();
|
||||
assert.equal(u.mem.mantraS3.grants, 0, `T${tier}: 3 grants used`);
|
||||
const g = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'palsy').length;
|
||||
inside.skill.gainSp(999); inside.skill.activate('test');
|
||||
h.step();
|
||||
assert.equal(h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'palsy').length, g, `T${tier}: no 4th grant`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 无言为真: an enemy holding 3 麻痹 entering her range keeps 2 and bounces the third; 溢出麻痹 left when it leaves the range or the skill ends all bounce at once; a 麻痹免疫 enemy counts the stacks she gives it', () => {
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2 });
|
||||
cast(u);
|
||||
h.run(0.2); // the grant of 0.1 s finds nobody (kept, never fired again here)
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 12] }); // off her range
|
||||
h.b.applyStatus(e, 'palsy', { value: 3 });
|
||||
h.step();
|
||||
assert.equal(palsy(e), 3, 'off her range: cap 3');
|
||||
e.x = 7; h.step();
|
||||
assert.equal(palsy(e), 2, 'on her range: 2');
|
||||
assert.equal(tagged(h, u, 'mantraOverflow').filter((c) => c.target === e).length, 1, 'the third bounced');
|
||||
// a neural burst's 3 麻痹 on a holder at the cap: 3 溢出麻痹 — one at once, two left; it leaves ⇒ both at once
|
||||
const n0 = tagged(h, u, 'mantraOverflow').length;
|
||||
h.b.applyStatus(e, 'palsy', { value: 3 });
|
||||
assert.equal(tagged(h, u, 'mantraOverflow').length - n0, 1, 'one at once');
|
||||
e.x = 13; h.step();
|
||||
assert.equal(tagged(h, u, 'mantraOverflow').length - n0, 3, 'the two left at once when it leaves');
|
||||
assert.equal(palsy(e), 2, 'its stacks stay (cap 3 again)');
|
||||
// the skill ends with 溢出麻痹 held: all of it bounces
|
||||
const f = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.step();
|
||||
h.b.applyStatus(f, 'palsy', { value: 3 });
|
||||
h.b.applyStatus(f, 'palsy', { value: 3 });
|
||||
const n1 = tagged(h, u, 'mantraOverflow').filter((c) => c.target === f).length;
|
||||
u.skill.end('test');
|
||||
assert.equal(tagged(h, u, 'mantraOverflow').filter((c) => c.target === f).length - n1, 2, 'the rest at the end');
|
||||
// 麻痹免疫: her grants count (ineffective stacks), so the second grant overflows
|
||||
const { h: h2, u: u2 } = field({ tier: 6, elite: true, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_08_a', row: 11, col: 6 }] });
|
||||
const imm = h2.spawn('enemy_immune', { pos: [10, 7] });
|
||||
cast(u2);
|
||||
h2.run(0.2);
|
||||
assert.equal(palsy(imm), 0, 'no 麻痹 buff (immune)');
|
||||
assert.equal(imm.mem.mantraPalsy, 2, 'counted');
|
||||
const ally = h2.unit(2);
|
||||
ally.skill.gainSp(999); ally.skill.activate('test');
|
||||
h2.step();
|
||||
assert.equal(tagged(h2, u2, 'mantraOverflow').filter((c) => c.target === imm).length, 1, 'its 溢出麻痹 bounces');
|
||||
done(h);
|
||||
done(h2);
|
||||
});
|
||||
|
||||
test('T1 噤声限域: an enemy whose attack a 麻痹 stack interrupts takes 135 % (PRI-X stage 3: 160 %) of her ATK as 元素伤害 (tagged 持续伤害); 10 % / 15 % of the interrupts keep the stack; not for an enemy she cannot select (隐匿)', () => {
|
||||
for (const f of [[5, false, null], [6, true, null], [5, true, PRIX], [6, true, PRIX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t = talentOf(tier, elite, mod, 0);
|
||||
assert.deepEqual(t, elite && mod === PRIX && tier === 6 ? { atk_scale: 1.6, prob: 0.15 } : { atk_scale: 1.35, prob: 0.1 }, label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 13 });
|
||||
const e = h.spawn('enemy_shooter', { pos: [10, 8] });
|
||||
h.b.applyStatus(u, 'disarm', { duration: 999, source: null }); // her attacks would kill nothing, but keep it clean
|
||||
h.run(0.6); // the aura's 0.5 s check has marked it
|
||||
let interrupts = 0, kept = 0;
|
||||
h.b.on('palsyTrigger', (c) => { if (c.enemy === e) { interrupts++; } });
|
||||
for (let i = 0; i < 400; i++) {
|
||||
h.b.applyStatus(e, 'palsy', { value: 1 });
|
||||
const before = interrupts;
|
||||
h.runUntil(() => interrupts > before, 3);
|
||||
h.step();
|
||||
if (palsy(e) > 0) { kept++; h.b.removeBuff(e, 'palsy'); }
|
||||
}
|
||||
const hush = tagged(h, u, 'mantraHush');
|
||||
assert.equal(hush.length, interrupts, `${label(f)}: one hit per interrupt`);
|
||||
for (const c of hush.slice(0, 5)) {
|
||||
approx(c.amount, u.s.atk * t.atk_scale, `${label(f)}: ${t.atk_scale * 100} % ATK`);
|
||||
assert.deepEqual([c.type, c.dmg.element, c.dmg.tags.includes('dot')], ['elemental', 'neural', true]);
|
||||
}
|
||||
assert.ok(Math.abs(kept / interrupts - t.prob) < 0.05, `${label(f)}: kept ${kept} / ${interrupts} ≈ ${t.prob}`);
|
||||
done(h);
|
||||
}
|
||||
// 隐匿: no mark, no hit; during S3 the check ignores 隐匿
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2, seed: 1 });
|
||||
const e = h.spawn('enemy_shooter', { pos: [10, 2] }); // behind her: it shoots her, she cannot see it (and S3 does not reveal it)
|
||||
h.b.addBuff(e, { key: 'test:stealth', flags: { stealth: true } });
|
||||
let interrupts = 0;
|
||||
h.b.on('palsyTrigger', (c) => { if (c.enemy === e) interrupts++; });
|
||||
h.run(0.6);
|
||||
h.b.applyStatus(e, 'palsy', { value: 2 });
|
||||
h.run(2.2);
|
||||
assert.equal(interrupts, 2, 'its attacks were interrupted');
|
||||
assert.equal(tagged(h, u, 'mantraHush').length, 0, 'a 隐匿 enemy: no effect');
|
||||
cast(u);
|
||||
h.run(0.6);
|
||||
assert.ok(!e.s.flags.reveal, 'not revealed (off her range)');
|
||||
h.b.applyStatus(e, 'palsy', { value: 2 });
|
||||
h.run(2.2);
|
||||
assert.equal(tagged(h, u, 'mantraHush').length, 2, 'S3: the check ignores 隐匿');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('T2 全局洞悉: the 侵入点 nearest to her (Manhattan) — every enemy that comes out of it gets 1 麻痹 0.1 s later (隐匿 too); the other gate\'s do not', () => {
|
||||
for (const [row, gate, other] of [[10, 0, 1], [11, 1, 0]]) { // flat stage gates: route 0 starts at (9,10), route 1 at (12,10)
|
||||
const { h, u } = field({ tier: 5, skill: 0, row, seed: 2 });
|
||||
assert.deepEqual(u.mem.mantraGate, gate === 0 ? [9, 10] : [12, 10], `row ${row}: the nearest gate`);
|
||||
const a = h.spawn('enemy_walker', { routeIndex: gate });
|
||||
const b = h.spawn('enemy_walker', { routeIndex: other });
|
||||
h.b.addBuff(a, { key: 'test:stealth', flags: { stealth: true } });
|
||||
h.step();
|
||||
assert.equal(palsy(a), 0, 'not at once');
|
||||
h.run(0.1);
|
||||
assert.deepEqual([palsy(a), palsy(b)], [1, 0], `row ${row}`);
|
||||
done(h);
|
||||
}
|
||||
// a tie (two gates at the same distance) is drawn at random
|
||||
const picks = new Set();
|
||||
for (let seed = 1; seed <= 12; seed++) {
|
||||
const { h, u } = field({ tier: 5, skill: 0, row: 10, col: 10, seed, stage: { rows: { 11: '##hrrrrrrrSrrrrrrrf##' } } });
|
||||
picks.add(String(u.mem.mantraGate));
|
||||
done(h);
|
||||
}
|
||||
assert.deepEqual([...picks].sort(), ['11,10', '9,10'], 'both gates one tile away get drawn');
|
||||
});
|
||||
|
||||
test('PRI-X “中枢神经探测模块” (stages 1 and 3): damage to an enemy in an element burst ×1.1; none without it', () => {
|
||||
for (const f of [[5, true, PRIX], [6, true, PRIX], [6, true, null], [5, false, null]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 8 });
|
||||
const plain = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.run(1.7);
|
||||
const n0 = atkHits(h, u).length;
|
||||
h.b.addBuff(plain, { key: 'burnBurst', duration: 99, flags: { burstLock: true } });
|
||||
h.run(1.7);
|
||||
const a = atkHits(h, u)[0].amount, b = atkHits(h, u).slice(n0)[0].amount;
|
||||
approx(b / a, elite && mod === PRIX ? 1.1 : 1, label(f));
|
||||
if (elite && mod === PRIX) assert.equal(modOf(tier, PRIX).traitOverride.bb.damage_scale, 1.1);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
// test/content/op_veen.test.js — the 自选 operator kit of 维伊 (char_4226_veen, 6★ 秘术师; kit
|
||||
// server/sim/content/kits/ops/op-veen.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in
|
||||
// every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or MSC-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_veen.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 VEEN = 'char_4226_veen';
|
||||
const FORMS = BACKUPS.units[VEEN].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const MSCY = 'uniequip_002_veen';
|
||||
const S1 = 'skchr_veen_1', S2 = 'skchr_veen_2', S3 = 'skchr_veen_3';
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const talentOf = (tier, elite, mod, i) => {
|
||||
const m = elite ? modOf(tier, mod) : null;
|
||||
return m?.talentChanges.find((t) => t.talentIndex === i)?.bb ?? formOf(tier, elite).talents.find((t) => t.index === i).bb;
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }) };
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, MSCY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5 } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'attack', 'ammoUsed'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: VEEN, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
|
||||
const warTicks = (h, u, e) => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && (c.dmg?.tags || []).includes('veenWar'));
|
||||
const stored = (u) => ({ s: u.trait.veen?.s ?? 0, t: u.trait.veen?.t ?? 0 });
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Run until she holds `t` 转置能量 and `s` stored energies (no enemy on the field). */
|
||||
function storeUp(h, u, t, s) {
|
||||
assert.ok(h.runUntil(() => stored(u).t === t && stored(u).s === s, 60), `stores up to ${t}/${s}`);
|
||||
}
|
||||
|
||||
test('维伊 in every 自选 form: her operator kit (all three skills authored), stats + MSC-Y attributes, 3-14 range, ranged arts every 3 s that hits air units, targetable by ground enemies, 空 bond, no 特质', () => {
|
||||
assert.equal(OPERATOR_KITS[VEEN], KITS[VEEN]);
|
||||
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], [VEEN, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat, u.base.cost], [1, 'ranged', 'arts', true, 3, 27], `${label(f)}: 秘术师`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-14`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk, FORMS['2/60/7/1'].trait.bb], [1142, 1320, { times: 9, merge_cnt: 3 }]);
|
||||
assert.deepEqual([modOf(5, MSCY).attr, modOf(6, MSCY).attr], [{ maxHp: 80, atk: 85 }, { maxHp: 190, atk: 120 }]);
|
||||
});
|
||||
|
||||
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(VEEN));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(VEEN), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: VEEN, skillIndex: 1, uniEquipId: MSCY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: VEEN, skillIndex: 1, uniEquipId: MSCY } } });
|
||||
});
|
||||
|
||||
test('trait + T1: with no target her attack check stores one energy (an attack action: the interval restarts — PRTS 分支特性信息), 3 merge into a 转置能量, at most 3 of those (then she stops); ATK +10 % while she holds one, ASPD +15 while she holds none; MSC-Y: ASPD +30 while she holds any energy', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = talentOf(tier, elite, mod, 0);
|
||||
assert.deepEqual(t0, { atk: 0.1, attack_speed: 15 }, label(f));
|
||||
const msc = elite && mod === MSCY ? 30 : 0;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
u.skill.sp = 0;
|
||||
assert.deepEqual(stored(u), { s: 1, t: 0 }, `${label(f)}: her first attack check (at deployment) stores at once`);
|
||||
approx(u.s.aspd, 100 + 15 + msc, `${label(f)}: no 转置能量: ASPD +15 (MSC-Y +30 while holding)`);
|
||||
approx(u.s.atk, u.base.atk, `${label(f)}: no ATK bonus`);
|
||||
const t1 = h.b.time;
|
||||
h.runUntil(() => stored(u).s === 2, 10);
|
||||
approx(h.b.time - t1, 3 * 100 / 115, `${label(f)}: the next one an attack interval later (set at the store, before MSC-Y)`, 0.03);
|
||||
const t2 = h.b.time;
|
||||
h.runUntil(() => stored(u).t === 1, 10);
|
||||
approx(h.b.time - t2, 3 * 100 / (115 + msc), `${label(f)}: then at the interval of ASPD ${115 + msc}`, 0.03);
|
||||
storeUp(h, u, 1, 0);
|
||||
approx(u.s.aspd, 100 + msc, `${label(f)}: a 转置能量: the ASPD +15 goes`);
|
||||
approx(u.s.atk, u.base.atk * 1.1, `${label(f)}: ATK +10 %`);
|
||||
storeUp(h, u, 3, 0);
|
||||
h.run(30);
|
||||
assert.deepEqual(stored(u), { s: 0, t: 3 }, `${label(f)}: no more storing at 3 转置能量`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('the volley: 转置能量 first (300 % ATK each), the main attack (100 %), then the stored energies (100 %) — one target, all landing together, each with the ATK it left with (+10 % while a 转置能量 was held)', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
u.skill.sp = 0;
|
||||
storeUp(h, u, 1, 2);
|
||||
const atk = u.s.atk;
|
||||
approx(atk, u.base.atk * 1.1, `T${tier}: holding a 转置能量`);
|
||||
const e = h.spawn('enemy_fly', { pos: [10, 7] });
|
||||
const a0 = atkHits(h, u).length;
|
||||
h.runUntil(() => atkHits(h, u).length > a0, 4);
|
||||
h.step();
|
||||
const hits = atkHits(h, u).slice(a0);
|
||||
assert.deepEqual(hits.map((c) => Math.round(c.amount / atk * 100) / 100), [3, 1, 1, 1], `T${tier}: 300 % + 100 % + 2 × 100 % at the cached ATK`);
|
||||
assert.ok(hits.every((c) => c.target === e && c.t === hits[0].t && c.dmg.attackId === hits[0].dmg.attackId && c.type === 'arts'), `T${tier}: one attack, one target (a flyer), together`);
|
||||
assert.deepEqual(stored(u), { s: 0, t: 0 }, `T${tier}: spent`);
|
||||
h.run(0.2);
|
||||
approx(u.s.atk, u.base.atk, `T${tier}: the ATK bonus gone after the volley`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 战争技艺: each hit adds a stack (a 转置能量 3) of 90 arts / s for 5 s, at most 3 (MSC-Y stage 3: 120 / s, at most 4); she never targets a marked enemy — with only marked ones in range she stores energy', () => {
|
||||
for (const f of [[5, false, null], [5, true, MSCY], [6, true, MSCY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t1 = talentOf(tier, elite, mod, 1);
|
||||
const s3m = elite && mod === MSCY && tier === 6;
|
||||
assert.deepEqual(t1, s3m ? { 'attack@duration': 5, 'attack@value': 120, 'attack@max_stack_cnt': 4 } : { 'attack@duration': 5, 'attack@value': 90, 'attack@max_stack_cnt': 3 }, label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0 });
|
||||
u.skill.sp = 0;
|
||||
h.run(3.1);
|
||||
u.trait.veen = null; // (start the check with nothing stored)
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.runUntil(() => atkHits(h, u).length > 0, 4);
|
||||
h.step();
|
||||
const war = a.findBuff(`veen:war:${u.id}`);
|
||||
assert.ok(war && war.stacks === 1, `${label(f)}: 1 stack`);
|
||||
h.run(1.02);
|
||||
const ticks = warTicks(h, u, a);
|
||||
assert.equal(ticks.length, 1, `${label(f)}: a tick per second`);
|
||||
approx(ticks[0].amount, t1['attack@value'], `${label(f)}: ${t1['attack@value']} arts`);
|
||||
// marked: not targeted — she stores instead while it is her only enemy in range
|
||||
const n0 = atkHits(h, u).length;
|
||||
h.run(2.9);
|
||||
assert.equal(atkHits(h, u).length, n0, `${label(f)}: no attack on a marked enemy`);
|
||||
assert.ok(stored(u).s >= 1, `${label(f)}: her next attack check stored an energy instead`);
|
||||
// another enemy: targeted at once; the mark lapses 5 s after the last stack
|
||||
const b = h.spawn('enemy_dummy', { pos: [9, 7] });
|
||||
h.runUntil(() => atkHits(h, u).length > n0, 4);
|
||||
assert.ok(atkHits(h, u).slice(n0).every((c) => c.target === b), `${label(f)}: the unmarked one`);
|
||||
assert.ok(h.runUntil(() => !a.findBuff(`veen:war:${u.id}`), 6), `${label(f)}: 5 s`);
|
||||
assert.equal(warTicks(h, u, a).length, 5, `${label(f)}: 5 ticks`);
|
||||
done(h);
|
||||
}
|
||||
// a 转置能量 gives 3 stacks; the cap
|
||||
for (const [tier, mod, cap] of [[6, null, 3], [6, MSCY, 4]]) {
|
||||
const { h, u } = field({ tier, elite: true, mod, skill: 0 });
|
||||
u.skill.sp = 0;
|
||||
storeUp(h, u, 2, 0);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.runUntil(() => atkHits(h, u).length >= 3, 4);
|
||||
h.step();
|
||||
assert.equal(e.findBuff(`veen:war:${u.id}`).stacks, cap, `T6 ${mod ?? 'none'}: 2 × 3 + 1 stacks capped at ${cap}`);
|
||||
h.run(1.02);
|
||||
approx(warTicks(h, u, e)[0].amount, talentOf(6, true, mod, 1)['attack@value'] * cap, `${cap} stacks`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 “自呼号生发” (AUTO): ASPD +60 / +75 for 18 s, cast as soon as its SP is full (31 / 29 from 7 / 9) — no enemy needed', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
const { h, u } = field({ tier, elite, skill: 0 });
|
||||
assert.deepEqual([u.skill.rule, u.skill.spCost, sk.initSp, sk.duration, sk.bb.attack_speed], ['SP_FULL', elite ? 29 : 31, elite ? 9 : 7, 18, elite ? 75 : 60], `T${tier}`);
|
||||
h.run(sk.spCost - sk.initSp - 0.5);
|
||||
assert.equal(u.skill.activations, 0);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: at full SP, no enemy`);
|
||||
const base = u.s.aspd - sk.bb.attack_speed;
|
||||
assert.ok(base === 115 || base === 100, `T${tier}: +${sk.bb.attack_speed} on top of ${base}`);
|
||||
approx(u.skill.timeLeft, 18, `T${tier}: 18 s`, 0.01);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 “以鲜血洗去” (MANUAL, data DEFAULT): 28 s, attack interval 3.0 − 0.5 s; each released shot +9 % / +11 % ATK and +6 / +7 ASPD (a 转置能量 3 stacks), at most 8 / 9 — later shots of a volley carry the stacks of the earlier ones', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
const b = sk.bb;
|
||||
const per = b['attack@veen_s_2_buff[stack].atk'], perAs = b['attack@veen_s_2_buff[stack].attack_speed'], max = b['attack@veen_s_2_buff[stack].max_stack_cnt'];
|
||||
assert.deepEqual([sk.duration, b.base_attack_time, per, perAs, max], [28, -0.5, elite ? 0.11 : 0.09, elite ? 7 : 6, elite ? 9 : 8], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
u.skill.sp = 0;
|
||||
storeUp(h, u, 1, 1);
|
||||
const base = u.base.atk;
|
||||
u.skill.gainSp(999);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 4), `T${tier}: cast as she attacks`);
|
||||
h.step(2);
|
||||
approx(u.s.interval, 2.5 * 100 / u.s.aspd, `T${tier}: base interval 2.5 s`);
|
||||
const hits = atkHits(h, u);
|
||||
h.runUntil(() => atkHits(h, u).length >= 3, 4);
|
||||
const v = atkHits(h, u).slice(0, 3);
|
||||
// 转置能量 (0 stacks, +10 %), main (3 stacks), stored (4 stacks) — every one +10 % (a 转置能量 was held)
|
||||
approx(v[0].amount, base * (1 + 0.1) * 3, `T${tier}: 转置能量 before any stack`);
|
||||
approx(v[1].amount, base * (1 + 0.1 + 3 * per), `T${tier}: the main shot after 3`);
|
||||
approx(v[2].amount, base * (1 + 0.1 + 4 * per), `T${tier}: the stored one after 4`);
|
||||
assert.ok(hits.length <= 3);
|
||||
approx(u.s.aspd, 100 + 15 + 5 * perAs, `T${tier}: 5 stacks of ASPD (and T1's +15 again)`);
|
||||
h.run(25);
|
||||
assert.equal(u.findBuff('veen:s2stack').mods.atkPct, per * max, `T${tier}: at most ${max}`);
|
||||
h.runUntil(() => !u.skill.active, 10);
|
||||
assert.equal(u.findBuff('veen:s2stack'), null, `T${tier}: gone with the skill`);
|
||||
void e;
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 “用赤铁铭记” (MANUAL, data DEFAULT): 18 / 21 bullets, ASPD +110 / +140; an attack spends 1 bullet per stored energy and 3 per 转置能量 (none without), its stored shots deal nothing and its main hit bounces that many times — every 0.5 s within 1.7 tiles, a new enemy first — for 130 % / 140 % of the cached ATK, each with a 战争技艺 stack', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const b = sk.bb;
|
||||
assert.deepEqual([b['attack@trigger_time'], b.attack_speed, b['attack@bounce_atk_scale'], b['attack@base_atk_scale']], [elite ? 21 : 18, elite ? 140 : 110, elite ? 1.4 : 1.3, 1], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 2 });
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
u.skill.sp = 0;
|
||||
storeUp(h, u, 1, 1);
|
||||
const atk = u.s.atk;
|
||||
u.skill.gainSp(999);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
const near = h.spawn('enemy_fly', { pos: [10, 8.2] });
|
||||
const far = h.spawn('enemy_dummy', { pos: [10, 12] }); // out of her range and 3.8 from the target
|
||||
assert.ok(h.runUntil(() => u.skill.active, 4), `T${tier}: cast as she attacks`);
|
||||
assert.deepEqual([u.skill.kind, u.skill.ammoMax], ['ammo', b['attack@trigger_time']], `T${tier}`);
|
||||
approx(u.s.aspd, 100 + b.attack_speed + 15, `T${tier}: ASPD (+ T1's 15: the volley spent the 转置能量)`, 1e-9);
|
||||
h.run(2.2);
|
||||
// the first attack: 1 + 3 = 4 bullets, the main hit then 4 bounces (e ↔ near: a new enemy first, then back)
|
||||
assert.equal(u.skill.ammoLeft, b['attack@trigger_time'] - 4, `T${tier}: 4 bullets`);
|
||||
assert.equal(h.hooksOf('ammoUsed').filter((c) => c.unit === u).length, 4);
|
||||
const hits = atkHits(h, u);
|
||||
const id = hits[0].dmg.attackId;
|
||||
const of = hits.filter((c) => c.dmg.attackId === id);
|
||||
assert.equal(of.length, 5, `T${tier}: main + 4 bounces, no stored-shot damage`);
|
||||
approx(of[0].amount, atk * b['attack@base_atk_scale'], `T${tier}: the main hit`);
|
||||
assert.deepEqual(of.map((c) => c.target.id), [e.id, near.id, e.id, near.id, e.id], `T${tier}: a new enemy first, then between them`);
|
||||
for (const c of of.slice(1)) approx(c.amount, atk * b['attack@bounce_atk_scale'], `T${tier}: bounce at the cached ATK`);
|
||||
for (let i = 2; i < of.length; i++) approx(of[i].t - of[i - 1].t, 0.5, `T${tier}: 0.5 s apart`, 0.08);
|
||||
assert.ok(!of.some((c) => c.target === far), `T${tier}: 1.7 tiles at most`);
|
||||
assert.equal(near.findBuff(`veen:war:${u.id}`).stacks, 2, `T${tier}: a 战争技艺 stack per bounce`);
|
||||
// attacks without stored energy spend nothing; the skill ends when the bullets run out
|
||||
const left = u.skill.ammoLeft;
|
||||
h.run(1.5);
|
||||
assert.ok(u.skill.ammoLeft <= left);
|
||||
h.runUntil(() => !u.skill.active, 300);
|
||||
assert.equal(u.skill.ammoLeft, 0, `T${tier}: ended out of bullets`);
|
||||
assert.equal(h.hooksOf('skillEnd').filter((c) => c.unit === u)[0].reason, 'ammo');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
+331
-118
@@ -1107,126 +1107,126 @@
|
||||
"snaps": "00fbd370 8c991923 96dc2bea 8d706ab9 43830855 d2a46ee0 09d5fe80 fd5bcc83 a88f6aca 1077c610 42023519 3f71e919 f0900b6b 05d99ec6 f7f79850 b612552c 23d34fc4 0835ffb5 8536e543 3091e242"
|
||||
},
|
||||
"diy-028": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=narant/S1/002_narant 6_diy2_b=narant/S2/002_narant 6_diy1_b=narant/S3/002_narant 6_diy2_b=narant/S1/003_narant 6_diy1_b=narant/S2/003_narant 6_diy2_b=narant/S3/003_narant 5_diy1_a=csnipe 5_diy2_b=csnipe",
|
||||
"fast": true,
|
||||
"about": "6_diy1_b=narant/S1/002_narant 6_diy2_b=narant/S2/002_narant 6_diy1_b=narant/S3/002_narant 6_diy2_b=narant/S1/003_narant 6_diy1_b=narant/S2/003_narant 6_diy2_b=narant/S3/003_narant 5_diy1_a=logos/S1 5_diy2_a=logos/S2 5_diy1_a=logos/S3 5_diy2_b=logos/S1 5_diy1_b=logos/S2 5_diy2_b=logos/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m06",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 13,
|
||||
"seed": 2789905475,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 209.47,
|
||||
"ticks": 6284,
|
||||
"time": 216.83,
|
||||
"ticks": 6505,
|
||||
"reason": "cleared",
|
||||
"killed": 9,
|
||||
"killed": 24,
|
||||
"total": 147,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 121,
|
||||
"rngDraws": 233,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":9,"total":147,"perfect":false,"leaks":138,"leaksUncounted":0,"coins":0,"dmg":730326,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":24,"total":147,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":1244770,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,97,0,654,4256,0,4,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,169,0,14365,10002,0,18,0],
|
||||
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_narant",2331,1,173,1,67234,4662,0,20,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_narant",0,0,108,1,110299,4662,0,26,1],
|
||||
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_narant",0,0,127,3,174608,4662,0,43,2],
|
||||
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_narant",0,0,94,1,122987,5262,0,29,2],
|
||||
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_narant",0,0,88,0,83429,5262,0,28,2],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_narant",0,0,143,3,147500,5262,0,39,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,159,0,5253,3813,0,18,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,153,0,3998,4530,0,14,0]
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_narant",2331,1,170,2,83624,4662,0,30,2],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_narant",2331,1,171,2,135821,4662,0,46,1],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_narant",1576.39,1,140,6,346892,3086,0,94,2],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_narant",0,0,152,2,116728,5262,0,21,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_narant",0,0,162,7,416081,5262,0,81,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_narant",0,0,10,4,109274,2631,0,27,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,78,0,3165,2594,0,6,0],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,94,0,6652,2594,0,14,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,84,0,5176,2594,0,9,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,83,0,3742,3080,0,7,0],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,4,1,10707,1540,0,21,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,89,0,6908,3080,0,12,0]
|
||||
],
|
||||
"enemies": {"enemy_1010_demon_2":[36,1,35,22806,317498],"enemy_1071_dftman":[51,6,45,1753,194180],"enemy_1072_dlancer":[60,2,58,27814,218648]},
|
||||
"hooks": [1,173,6284,330,330,634,617,0,32,170,10,9,0,7331,124,30,147,138,1,32,0,0,17],
|
||||
"events": {"atk":330,"deploy":26,"die":32,"dmg":617,"fx":166,"leak":138,"skill":19,"spawn":159,"status":199},
|
||||
"fx": {"aoe":34,"bounce":94,"dodge":38},
|
||||
"snaps": "46ac6de1 2112c33e 837a4ca6 d3ee52bf c1ba00c7 a9527591 0ab35d3e a6d4d348 ef74a88f 9464be05 0c0cf77b 9fe08ceb d1faeac8 d57e45f0 3f5a7950 d5f0437e 97116a6c f3594fa0 3e97535d e3b4a06c 09cf44e6"
|
||||
"enemies": {"enemy_1010_demon_2":[36,1,35,14202,495297],"enemy_1071_dftman":[51,14,37,5132,306556],"enemy_1072_dlancer":[60,9,51,21712,442917]},
|
||||
"hooks": [1,171,6505,468,468,1005,993,0,45,168,11,10,0,6647,224,37,147,123,1,45,0,0,12],
|
||||
"events": {"atk":468,"deploy":24,"die":45,"dmg":993,"fx":205,"leak":123,"skill":21,"spawn":159,"status":332},
|
||||
"fx": {"aoe":46,"bounce":89,"dodge":43,"logosLexicon":27},
|
||||
"snaps": "908a1f91 4582d7f2 dd208a1a 134a98fb 67558ccf 5bad995e b3ce5027 65152964 2f0c174b dc8e2347 0a686aab cca266d7 2dfb1ca0 47547715 c67debba 8c8f23d2 b5a4d136 ca6db6ee 8d8f8837 eaa96e1d 7c2c78bb 4dcf0b88"
|
||||
},
|
||||
"diy-029": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
|
||||
"about": "5_diy1_b=logos/S1/002_logos 5_diy2_b=logos/S2/002_logos 5_diy1_b=logos/S3/002_logos 5_diy2_b=logos/S1/003_logos 5_diy1_b=logos/S2/003_logos 5_diy2_b=logos/S3/003_logos 6_diy1_a=logos/S1 6_diy2_a=logos/S2 6_diy1_a=logos/S3 6_diy2_b=logos/S1 6_diy1_b=logos/S2 6_diy2_b=logos/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m07",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"round": 13,
|
||||
"seed": 2642056114,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 203.93,
|
||||
"ticks": 6118,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 195.3,
|
||||
"ticks": 5859,
|
||||
"reason": "cleared",
|
||||
"killed": 35,
|
||||
"total": 114,
|
||||
"killed": 70,
|
||||
"total": 78,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 61,
|
||||
"rngDraws": 1022,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":35,"total":114,"perfect":false,"leaks":79,"leaksUncounted":0,"coins":0,"dmg":351370,"bossDmg":0,"heal":0,"deaths":14,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":70,"total":78,"perfect":false,"leaks":8,"leaksUncounted":0,"coins":0,"dmg":941257,"bossDmg":0,"heal":0,"deaths":0,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,115,1,5012,5208,0,65,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,107,0,6422,6742,0,32,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,96,7,33528,3058,0,33,4],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,91,20,167275,3808,0,92,5],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,97,3,27895,5276,0,49,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,92,4,65144,6822,0,60,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,0,13450,0,0,96,3],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,7,0,15482,0,0,94,3],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,9,0,0,0,0,0,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,11,0,0,0,0,0,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,13,0,8532,0,0,72,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,110,0,8630,2262,0,62,2],
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_logos",1640,1,5,0,0,0,0,0,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_logos",1640,1,7,1,233,0,0,1,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_logos",1640,1,9,8,121206,0,0,35,3],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_logos",1540,1,11,8,56333,0,0,34,1],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_logos",1540,1,13,2,84853,0,0,92,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_logos",1540,1,6,19,136661,0,0,38,3],
|
||||
["p1",7,"op","chess_char_6_diy1_a",0,null,1297,1,8,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_6_diy2_a",1,null,1297,1,10,2,117732,0,0,161,3],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,1297,1,12,8,239570,0,0,118,3],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"none",1540,1,14,22,184669,0,0,119,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"none",1540,1,3,0,0,0,0,0,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"none",1540,1,4,0,0,0,0,0,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_1166_dusbr_2":[45,21,24,2758,102053],"enemy_1169_duphlx_2":[51,12,39,4945,126678],"enemy_1172_dugago":[18,2,16,25472,122640]},
|
||||
"hooks": [1,135,6118,1009,1009,1517,1496,0,49,128,26,26,0,26499,134,64,114,79,1,64,0,0,21],
|
||||
"events": {"atk":1009,"deploy":19,"die":49,"dmg":1496,"fx":188,"leak":79,"skill":52,"spawn":128,"status":414},
|
||||
"fx": {"aoe":46,"dodge":21,"dp":36,"revive":8,"slash":69,"stone":8},
|
||||
"snaps": "d294bf74 f726af71 b04a5d86 cc738f7b ae57a036 3981ef1a b09e1223 1d3de17a f52ea4c9 0c6bf8b5 dd94b32b 41e1f44b 7b8565f0 5f461c30 c6321a2a b5ad3265 5be21e97 4ddfcf65 c9263b7b a7a973da 60dd319e"
|
||||
"enemies": {"enemy_1045_hammer_2":[18,17,1,0,335969],"enemy_1061_zomshd":[60,53,7,0,605288]},
|
||||
"hooks": [1,92,5859,598,598,1005,1065,11667,70,78,17,13,0,27522,283,0,78,8,1,70,0,0,0],
|
||||
"events": {"atk":694,"deploy":12,"die":70,"dmg":1065,"fx":318,"leak":8,"skill":30,"spawn":92,"status":376},
|
||||
"fx": {"logosExecute":28,"logosLexicon":290},
|
||||
"snaps": "d77c72cd 6367c5ba ea565f8f 5f5df428 169ff00e b82e9767 e24922a1 68f5d155 8ca60410 956071bb 9e055166 b0ff6b34 e4adebd6 233ce084 49d5f3a4 73fcf051 acd5e35f 3539ff89 a8c65166 61f6bc02"
|
||||
},
|
||||
"diy-030": {
|
||||
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
|
||||
"about": "6_diy1_b=logos/S1/002_logos 6_diy2_b=logos/S2/002_logos 6_diy1_b=logos/S3/002_logos 6_diy2_b=logos/S1/003_logos 6_diy1_b=logos/S2/003_logos 6_diy2_b=logos/S3/003_logos 5_diy1_a=mantra/S1 5_diy2_a=mantra/S2 5_diy1_a=mantra/S3 5_diy2_b=mantra/S1 5_diy1_b=mantra/S2 5_diy2_b=mantra/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m01",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 12,
|
||||
"round": 13,
|
||||
"seed": 4282451111,
|
||||
"waveId": "act1autochess_h05",
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 24,
|
||||
"total": 42,
|
||||
"killed": 95,
|
||||
"total": 135,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 360,
|
||||
"rngDraws": 1919,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":24,"total":42,"perfect":false,"leaks":18,"leaksUncounted":0,"coins":0,"dmg":300294,"bossDmg":0,"heal":10910,"deaths":31,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":95,"total":135,"perfect":false,"leaks":40,"leaksUncounted":0,"coins":0,"dmg":1891121,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,6,3,41257,1865,0,30,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,58,4,47553,4748,0,34,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,74,0,0,11540,0,0,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",638.46,1,75,0,0,13203,0,0,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,2,0,64600,1272,0,50,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,3,2,91461,1889,0,46,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,4,0,7285,1372,0,5,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,48,1,34810,7510,0,11,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,8,0,0,3647,6959,14,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,43,0,0,6191,3951,6,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,1,0,6208,1160,0,10,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,5,0,7121,1430,0,9,1]
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_logos",1710,1,2,8,105758,0,0,91,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_logos",1710,1,3,7,82090,0,0,182,3],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_logos",1710,1,4,12,212399,0,0,128,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_logos",0,0,147,5,39603,3080,0,46,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_logos",0,0,143,1,98006,3080,0,154,3],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_logos",0,0,142,5,125493,3080,0,72,3],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,153,3,27386,2142,0,38,9],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,124,8,36511,2142,0,65,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,8,13,460179,1071,0,95,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,114,2,23725,2616,0,55,13],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,1,14,102616,1308,0,75,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,5,17,577355,1308,0,95,2]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[18,18,0,222,8571],"enemy_1005_yokai_3":[12,3,6,25395,152682],"enemy_9009_acfort":[12,3,9,30211,139041]},
|
||||
"hooks": [1,75,6900,410,410,661,694,35,55,70,10,28,0,9568,225,0,42,15,1,41,0,0,0],
|
||||
"events": {"atk":520,"deploy":33,"die":55,"dmg":694,"fx":57,"heal":31,"leak":15,"skill":20,"spawn":54,"status":74},
|
||||
"fx": {"aoe":5,"beam":46,"dodge":3,"spGain":3},
|
||||
"snaps": "d299cac5 9e5fecae 9fef873f 0041e6d5 bf4aa79e e9d81067 2b840ee7 5f2b2a5c 73fc1c64 a0d69be3 19bf79bd 438e2070 74bb8728 42487a28 de9f9caf 88600853 8c90dabd 84058f3d af1ad7d9 010e3167 6db40d66 e9645c1d 284f3878"
|
||||
"enemies": {"enemy_1288_duskls":[27,22,5,2249,245694],"enemy_1288_duskls_2":[57,38,18,3884,586159],"enemy_1292_duskld":[51,35,15,13695,1059268]},
|
||||
"hooks": [1,153,6900,1188,1188,3029,4105,0,110,148,44,43,0,25246,17814,125,135,38,1,247,0,3,0],
|
||||
"events": {"atk":1438,"deploy":18,"die":110,"dmg":4105,"fx":1191,"leak":38,"skill":87,"spawn":147,"status":1475},
|
||||
"fx": {"burst":3,"ember":137,"logosExecute":71,"logosLexicon":577,"mantraArc":300,"mantraGate":9,"mantraGrant":12,"palsy":47,"revive":35},
|
||||
"snaps": "ccefd180 74e321db 643097c1 db0e6e62 93c71528 cc5c27dc e099974a 9b647244 f1270822 4be2b7c9 98c43f6a 31a51556 caaffcdd 92411235 a7a659a0 b284a518 4614387d 684ed5e1 e41ab6bb d32ae971 855c5d18 4444222e 1b7941fe"
|
||||
},
|
||||
"diy-031": {
|
||||
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
|
||||
"about": "5_diy1_b=mantra/S1/002_mantra 5_diy2_b=mantra/S2/002_mantra 5_diy1_b=mantra/S3/002_mantra 6_diy1_a=mantra/S1 6_diy2_a=mantra/S2 6_diy1_a=mantra/S3 6_diy2_b=mantra/S1 6_diy1_b=mantra/S2 6_diy2_b=mantra/S3 6_diy1_b=mantra/S1/002_mantra 6_diy2_b=mantra/S2/002_mantra 6_diy1_b=mantra/S3/002_mantra",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m02",
|
||||
@@ -1234,73 +1234,286 @@
|
||||
"round": 13,
|
||||
"seed": 1788669353,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 12,
|
||||
"time": 197.87,
|
||||
"ticks": 5936,
|
||||
"reason": "cleared",
|
||||
"killed": 36,
|
||||
"total": 36,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 211,
|
||||
"rngDraws": 89,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":12,"total":36,"perfect":false,"leaks":24,"leaksUncounted":0,"coins":0,"dmg":191861,"bossDmg":0,"heal":28856,"deaths":25,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":36,"total":36,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1905321,"bossDmg":0,"heal":0,"deaths":10,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,61,0,0,5208,0,0,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,5,0,0,3431,0,0,0],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,63.15,1,64,0,7369,5241,0,0,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,63,0,47963,8259,0,0,3],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,50,0,0,10086,0,0,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,44,0,0,21845,0,0,0],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,59,0,573,3816,0,1,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,62,0,71082,5806,0,45,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,36,1,9537,5112,0,6,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,4,0,55338,1819,0,10,1],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,53,0,0,3280,16322,22,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,47,0,0,4850,12534,19,1],
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_mantra",1398,1,6,8,154570,0,0,43,10],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_mantra",0,0,7,3,260388,1398,0,56,3],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_mantra",0,0,55,1,34956,4194,0,19,1],
|
||||
["p1",4,"op","chess_char_6_diy1_a",0,null,1071,1,4,1,75122,0,0,0,0],
|
||||
["p1",5,"op","chess_char_6_diy2_a",1,null,1071,1,5,3,136924,0,0,39,3],
|
||||
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,8,4,294265,1071,0,73,2],
|
||||
["p1",7,"op","chess_char_6_diy2_b",0,"none",401.08,1,52,0,43930,3523,0,21,4],
|
||||
["p1",8,"op","chess_char_6_diy1_b",1,"none",1308,1,9,0,186745,0,0,41,4],
|
||||
["p1",9,"op","chess_char_6_diy2_b",2,"none",866.05,1,46,6,248758,1750,0,33,2],
|
||||
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_mantra",0,0,10,3,128223,1453,0,27,6],
|
||||
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_mantra",0,0,11,2,233002,1453,0,53,3],
|
||||
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_mantra",561.67,1,3,3,108437,891,0,0,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_1005_yokai_2":[12,12,0,570,4443],"enemy_1005_yokai_3":[12,0,11,40977,83772],"enemy_9009_acfort":[12,0,12,37205,103646]},
|
||||
"hooks": [1,64,6900,320,320,443,409,74,37,60,9,9,0,11179,0,0,36,23,1,26,0,0,34],
|
||||
"events": {"atk":353,"deploy":26,"die":37,"dmg":409,"fx":120,"heal":68,"leak":23,"skill":18,"spawn":50,"status":28},
|
||||
"fx": {"aoe":3,"beam":37,"dodge":34,"dp":4,"slash":32,"spGain":10},
|
||||
"snaps": "8bb89486 af5a3103 edf9cdf6 9c8aa642 4004d4b6 398064e7 b94ae3bd e4f347a5 8cf52583 63ae2ad4 2d11279d 74b6b79f dbb9f658 f3eeb437 731e600c 26663686 39a333c1 595094aa 1bf50db7 39b3ef7d eee313e8 326f4d94 97a0c299"
|
||||
"enemies": {"enemy_1005_yokai_2":[12,12,0,1534,47800],"enemy_1005_yokai_3":[12,12,0,6840,1031956],"enemy_9009_acfort":[12,12,0,7359,825565]},
|
||||
"hooks": [1,55,5936,433,433,1812,2023,0,46,46,38,36,0,16252,187,0,36,0,1,44,0,24,0],
|
||||
"events": {"atk":458,"deploy":17,"die":46,"dmg":2023,"fx":206,"skill":74,"spawn":50,"status":97},
|
||||
"fx": {"beam":11,"burst":24,"mantraArc":59,"mantraGate":17,"mantraGrant":7,"palsy":88},
|
||||
"snaps": "51e27048 9cef1698 b61a98fa e92e9fb7 bc012497 8cfcc3ad 4bded9b4 c1266494 ed60128f 74df4566 d31c137a 998f967d 857e9ead 02820043 73d9434c e05a067e e0d63b9d c1e5b283 57132f29 b039284f"
|
||||
},
|
||||
"diy-032": {
|
||||
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
|
||||
"about": "5_diy1_a=veen/S1 5_diy2_a=veen/S2 5_diy1_a=veen/S3 5_diy2_b=veen/S1 5_diy1_b=veen/S2 5_diy2_b=veen/S3 5_diy1_b=veen/S1/002_veen 5_diy2_b=veen/S2/002_veen 5_diy1_b=veen/S3/002_veen 6_diy1_a=veen/S1 6_diy2_a=veen/S2 6_diy1_a=veen/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m03",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 13,
|
||||
"round": 12,
|
||||
"seed": 148338108,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 216.33,
|
||||
"ticks": 6490,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 202.7,
|
||||
"ticks": 6081,
|
||||
"reason": "cleared",
|
||||
"killed": 2,
|
||||
"total": 117,
|
||||
"killed": 23,
|
||||
"total": 114,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 69,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":2,"total":117,"perfect":false,"leaks":115,"leaksUncounted":0,"coins":0,"dmg":73158,"bossDmg":0,"heal":0,"deaths":25,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":23,"total":114,"perfect":false,"leaks":91,"leaksUncounted":0,"coins":0,"dmg":859805,"bossDmg":0,"heal":0,"deaths":12,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,112,0,14601,3730,0,11,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,5,2,33453,2494,0,9,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,146,0,14003,8078,0,53,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,141,0,7814,13356,0,8,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,131,0,2241,3480,0,6,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,145,0,1046,4350,0,2,0],
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,1215,1,8,3,86362,0,0,80,4],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1215,1,11,3,108771,0,0,83,3],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1215,1,13,1,120461,0,0,79,2],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",1404,1,15,0,111835,0,0,67,4],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",1404,1,4,7,146742,0,0,89,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1404,1,9,6,200718,0,0,121,1],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_veen",0,0,111,1,11132,2968,0,4,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_veen",0,0,87,0,25030,2968,0,6,0],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_veen",0,0,88,1,15957,2968,0,8,0],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,1215,1,136,1,10296,2430,0,7,0],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,80,0,7685,2430,0,5,0],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,125,0,14816,2430,0,11,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_10031_cnvsld":[39,2,37,3710,53063],"enemy_1019_jshoot_2":[57,0,57,17616,17501],"enemy_9008_acbunn":[21,0,21,14162,2594]},
|
||||
"hooks": [1,146,6490,226,226,213,228,0,27,142,2,18,0,2389,10,23,117,115,1,27,0,0,0],
|
||||
"events": {"atk":238,"deploy":26,"die":27,"dmg":226,"fx":1,"leak":115,"skill":4,"spawn":126,"status":16},
|
||||
"fx": {"displace":1},
|
||||
"snaps": "038e9c71 b94393f2 b660dbec 471efee3 ea2c5cd8 67a95602 be0382a8 ed05227c 9d9b8d44 97b9ccc2 cebb0260 f89cd753 07c1d2ac a9ae2748 9b26f9da 162fa605 4b05eaa4 0287acaf ec32f6a6 40f84535 dad58c31 43eb8c7a"
|
||||
"enemies": {"enemy_10124_uashld_2":[12,0,12,1215,103016],"enemy_1043_zomsbr":[72,22,50,4183,401369],"enemy_1061_zomshd":[30,1,29,10796,355420]},
|
||||
"hooks": [1,136,6081,591,591,3365,3365,45973,35,126,19,18,39,24319,0,14,114,91,1,35,0,0,0],
|
||||
"events": {"atk":591,"deploy":19,"die":35,"dmg":3365,"fx":96,"leak":91,"skill":37,"spawn":129},
|
||||
"fx": {"veenBounce":42,"veenVolley":54},
|
||||
"snaps": "10719944 a54473e5 586c12a4 e8572e94 cd658a5e 022bbca2 6875176a 88f39323 fe22255b b8659b38 b3436607 2bff9df5 448c5d3c bae393b0 7569d588 d5f887b5 1075cb4d 45d53303 6b53b401 8529d78b ee4d40ec"
|
||||
},
|
||||
"diy-033": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=veen/S1 6_diy2_b=veen/S2 6_diy1_b=veen/S3 6_diy2_b=veen/S1/002_veen 6_diy1_b=veen/S2/002_veen 6_diy2_b=veen/S3/002_veen 5_diy1_a=csnipe 5_diy2_b=csnipe",
|
||||
"fast": true,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m04",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"seed": 2802554051,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 59,
|
||||
"total": 96,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":59,"total":96,"perfect":false,"leaks":37,"leaksUncounted":0,"coins":0,"dmg":853050,"bossDmg":0,"heal":0,"deaths":11,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,108,0,912,6384,0,21,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,115,0,2232,8724,0,24,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,118,1,22685,5128,0,31,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,117,3,27195,10002,0,35,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",0,"none",1404,1,3,11,86649,0,0,79,5],
|
||||
["p1",6,"op","chess_char_6_diy2_b",1,"none",1404,1,4,20,114352,0,0,94,3],
|
||||
["p1",7,"op","chess_char_6_diy1_b",2,"none",1404,1,11,2,84750,0,0,27,2],
|
||||
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_002_veen",1594,1,13,2,65860,0,0,18,5],
|
||||
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_002_veen",1594,1,5,13,145332,0,0,99,4],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_veen",1594,1,10,5,200360,0,0,138,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,12,0,40062,0,0,192,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,14,0,62659,0,0,190,3],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1229_darmy":[27,19,8,1082,175575],"enemy_1229_darmy_2":[57,40,17,9636,509947],"enemy_9007_acelem":[12,0,11,19520,190361]},
|
||||
"hooks": [1,118,6900,975,975,3619,3644,0,70,106,29,27,55,34880,0,28,96,36,1,70,0,0,0],
|
||||
"events": {"atk":975,"deploy":20,"die":70,"dmg":3644,"fx":138,"leak":36,"skill":56,"spawn":110,"status":18},
|
||||
"fx": {"beam":10,"veenBounce":55,"veenVolley":73},
|
||||
"snaps": "a07829f2 3cf158b0 479435c7 27a9529c 96a9ad65 9d17fa2c 50b24bed 0fb108d6 9d492c99 ec0861cb a829d34a c0dd4419 990b0b71 cace0625 3408f7ff 4e21a576 c8ddbd12 63514ded f63b31be e6fa5bcd 041f97db ca21f4f6 983c1aa5"
|
||||
},
|
||||
"diy-034": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m01",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 12,
|
||||
"seed": 1162087510,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 206.8,
|
||||
"ticks": 6204,
|
||||
"reason": "cleared",
|
||||
"killed": 7,
|
||||
"total": 135,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 73,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":7,"total":135,"perfect":false,"leaks":128,"leaksUncounted":0,"coins":0,"dmg":532657,"bossDmg":0,"heal":31513,"deaths":17,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,107,1,4789,5208,0,27,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,124,0,12640,6742,0,39,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,151,0,38897,6450,0,23,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,100,1,230450,12913,0,47,4],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1707.81,1,127,2,58554,13912,0,65,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,129,2,53451,6822,0,31,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,4,1,31054,1133,0,78,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,158,0,40022,1360,0,85,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,11,0,0,4772,14398,36,3],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1021.64,1,13,0,0,4000,17115,38,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,116,0,17420,1864,0,52,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,14,0,45379,3000,0,120,3],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1148_dssbr":[63,4,59,20340,257178],"enemy_1148_dssbr_2":[60,3,57,21428,223440],"enemy_1439_dslntf":[12,0,12,26407,52039]},
|
||||
"hooks": [1,159,6204,888,888,1168,2159,74,24,152,26,25,0,27105,210,67,135,128,1,24,0,39,21],
|
||||
"events": {"atk":888,"deploy":21,"die":24,"dmg":2159,"fx":119,"heal":74,"leak":128,"skill":51,"spawn":150,"status":516},
|
||||
"fx": {"aoe":13,"burst":39,"dodge":21,"dp":6,"slash":40},
|
||||
"snaps": "8c950172 84292247 66bb7644 57694e16 77300d65 0485e386 b1644dc3 9fd8e152 ae38532d acf85cfd 6416e99b 150194af 384bffbd da563864 d3c6a232 e79c9e9f 752e3a91 4b57e564 3328cd07 f5dbb08c 0d910b50"
|
||||
},
|
||||
"diy-035": {
|
||||
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m02",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"seed": 4056308894,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 202.73,
|
||||
"ticks": 6082,
|
||||
"reason": "cleared",
|
||||
"killed": 87,
|
||||
"total": 99,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 1043,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":87,"total":99,"perfect":false,"leaks":12,"leaksUncounted":0,"coins":0,"dmg":702906,"bossDmg":0,"heal":55399,"deaths":3,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,535.6,1,3,20,91921,1329,0,125,3],
|
||||
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,1,6,64041,0,0,60,3],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,311,1,4,1,2114,843,0,24,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1218,1,75,1,8828,2132,0,35,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,5,11,85301,0,0,201,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,8,23,178332,0,0,186,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,6,0,18491,0,0,17,2],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,9,21,192348,0,0,77,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,10,0,0,16500,27423,53,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1021,1,11,0,0,15939,27976,69,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,12,4,30977,25283,0,109,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,2,0,30553,0,0,54,2]
|
||||
],
|
||||
"enemies": {"enemy_1010_demon":[42,41,1,54506,296286],"enemy_1320_wdrrl_2":[12,2,10,0,173583],"enemy_1422_lrsldr_2":[45,44,1,7520,233038]},
|
||||
"hooks": [1,112,6082,1234,1234,1797,1860,205,90,102,24,23,0,34456,6724,33,99,12,1,90,0,0,0],
|
||||
"events": {"atk":1488,"deploy":13,"die":90,"dmg":1859,"fx":32,"heal":158,"leak":12,"skill":46,"spawn":111,"status":624},
|
||||
"fx": {"aoe":14,"displace":1,"dodge":16,"spGain":1},
|
||||
"snaps": "9b12b5a2 9008ef38 d758917c a78c501f 8e5be89d 9d1491a1 be80716d ba16422b 24993631 17f367e0 5eb39615 1d861ce1 e35e2e74 f401a47c 00eb08a9 77c341e2 708eb8a2 328fa66d 2cc731ce c77d68c6 d21e342c"
|
||||
},
|
||||
"diy-036": {
|
||||
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m03",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"seed": 2415735691,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 202.87,
|
||||
"ticks": 6086,
|
||||
"reason": "cleared",
|
||||
"killed": 192,
|
||||
"total": 266,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 239,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":192,"total":266,"perfect":false,"leaks":74,"leaksUncounted":0,"coins":0,"dmg":435875,"bossDmg":0,"heal":48672,"deaths":10,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,227,4,3127,5208,0,22,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",3431,1,246,8,15036,12560,0,71,2],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1529,1,234,24,18577,4001,0,25,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1904,1,239,25,18823,4924,0,33,3],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,2638,1,163,13,46706,20174,0,77,3],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",3531,1,228,14,103845,18923,0,84,3],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,6,3432,0,0,27,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,12,18,30557,0,0,64,2],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,14,0,17500,0,0,23,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,80,178273,0,0,72,2],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,25511,56,2],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,154,0,0,6020,23161,35,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1195_sfyin": [39,25,14,2609,109226],
|
||||
"enemy_1196_msfyin": [50,39,11,0,85],
|
||||
"enemy_1197_sfshu": [48,37,11,8745,174749],
|
||||
"enemy_1198_msfshu": [111,85,26,0,287],
|
||||
"enemy_1207_sfji": [12,6,6,60456,151454],
|
||||
"enemy_1208_msfji": [6,0,6,0,74]
|
||||
},
|
||||
"hooks": [1,290,6086,856,856,1375,1336,160,202,276,25,25,0,31178,0,63,266,74,1,207,0,0,39],
|
||||
"events": {"atk":922,"deploy":21,"die":202,"dmg":1336,"fx":264,"heal":141,"leak":74,"skill":50,"spawn":281,"status":32},
|
||||
"fx": {"aoe":53,"dodge":39,"dp":55,"slash":40,"spGain":9,"summon":68},
|
||||
"snaps": "74c08ab0 d7fae2c7 e5fc0721 098ca8da 386839f8 1014e7e2 5b3f511c cae02b78 6c9c4314 3ef337b7 19e2c676 b3eebb59 5c2a3715 354e546e 6ca7ab30 6153fdd2 dc92916e 069961c0 9b27edc4 4c63bb80 c4485363"
|
||||
},
|
||||
"diy-037": {
|
||||
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m04",
|
||||
"modeId": "mode_multi_normal",
|
||||
"round": 13,
|
||||
"seed": 775009396,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 213.43,
|
||||
"ticks": 6403,
|
||||
"reason": "cleared",
|
||||
"killed": 8,
|
||||
"total": 114,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 217,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":8,"total":114,"perfect":false,"leaks":106,"leaksUncounted":0,"coins":0,"dmg":372337,"bossDmg":0,"heal":0,"deaths":22,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,140,0,29075,5595,0,27,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,92,4,105718,4988,0,41,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,139,0,26275,10386,0,34,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,141,0,56947,11872,0,72,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,4,3,62257,0,0,139,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,5,1,92065,0,0,150,3],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1381_winman":[27,5,22,5454,142346],"enemy_1381_winman_2":[48,3,45,5428,226363],"enemy_1387_winshd":[39,0,39,21959,171306]},
|
||||
"hooks": [1,141,6403,576,576,3039,3101,0,30,136,7,23,0,10542,12614,31,114,106,1,30,0,0,0],
|
||||
"events": {"atk":611,"deploy":24,"die":30,"dmg":2992,"fx":37,"leak":106,"skill":13,"spawn":123,"status":800},
|
||||
"fx": {"displace":27,"dodge":10},
|
||||
"snaps": "e840ece4 d3471b55 e3a6dada 611a7964 e06281b6 bd53a023 022502fe eddd554d 18dcff13 eb023c12 11b5046a b199b58b 0db4afa9 bc8a6002 65d5bf21 87a25c0c eed04c57 554a7c10 fdd40ba4 9fbc3909 521a5144 3a7d6770"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// test/sim/palsy-trigger.test.js — the engine hook `palsyTrigger` (server/sim/ai.js enemyAttack): fired when a 麻痹 stack
|
||||
// interrupts an enemy's normal attack (PRTS 真言 备注 "触发麻痹时指的是敌人的攻击被麻痹打断时"), once per interrupt; a
|
||||
// handler may keep the stack (`keep`) or kill the enemy. 真言's 噤声限域 (kits/ops/op-mantra.js) is its user.
|
||||
// Run: node --test test/sim/palsy-trigger.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const guard = chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 0, blockCnt: 2, maxHp: 1e6 }, skill: null });
|
||||
const shooter = enemyRec({ key: 'enemy_shooter', hp: 1e6, atk: 100, bat: 1, range: 3, speed: 0, mass: 0 });
|
||||
|
||||
function field(setup) {
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_guard: guard }, enemies: { enemy_shooter: shooter } },
|
||||
units: [{ chessId: 't_guard', row: 10, col: 5 }], content: 'none', autoFinish: false, setup,
|
||||
});
|
||||
h.step();
|
||||
return h;
|
||||
}
|
||||
|
||||
test('palsyTrigger: one event per interrupted attack, before the stack is used; no event without 麻痹', () => {
|
||||
const seen = [];
|
||||
const h = field((b) => b.on('palsyTrigger', (c) => seen.push({ t: b.time, stacks: c.buff.stacks, enemy: c.enemy })));
|
||||
const e = h.spawn('enemy_shooter', { pos: [10, 7] });
|
||||
h.run(3.05);
|
||||
assert.equal(seen.length, 0, 'no 麻痹, no event');
|
||||
const a0 = e.stats.attacks;
|
||||
h.b.applyStatus(e, 'palsy', { value: 2 });
|
||||
h.run(2.05);
|
||||
assert.deepEqual(seen.map((x) => [x.enemy, x.stacks]), [[e, 2], [e, 1]], 'each interrupt, the stack count before it is used');
|
||||
assert.equal(e.stats.attacks, a0, 'both attacks interrupted');
|
||||
assert.equal(e.findBuff('palsy'), null);
|
||||
h.run(1.1);
|
||||
assert.ok(e.stats.attacks > a0, 'attacks again');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('palsyTrigger: `keep` leaves the stack; a handler that kills the enemy ends its turn', () => {
|
||||
let keepNext = true;
|
||||
const h = field((b) => b.on('palsyTrigger', (c) => { if (keepNext) { c.keep = true; keepNext = false; } }));
|
||||
const e = h.spawn('enemy_shooter', { pos: [10, 7] });
|
||||
h.step();
|
||||
h.b.applyStatus(e, 'palsy', { value: 1 });
|
||||
const a0 = e.stats.attacks;
|
||||
h.run(1.1);
|
||||
assert.equal(e.findBuff('palsy')?.stacks, 1, 'kept');
|
||||
assert.equal(e.stats.attacks, a0, 'still interrupted');
|
||||
h.run(1.05);
|
||||
assert.equal(e.findBuff('palsy'), null, 'used by the next interrupt');
|
||||
assert.equal(e.stats.attacks, a0);
|
||||
// a lethal handler
|
||||
const h2 = field((b) => b.on('palsyTrigger', (c) => b.kill(c.enemy, null)));
|
||||
const e2 = h2.spawn('enemy_shooter', { pos: [10, 7] });
|
||||
h2.step();
|
||||
h2.b.applyStatus(e2, 'palsy', { value: 3 });
|
||||
h2.run(1.2);
|
||||
assert.ok(!e2.alive);
|
||||
assert.equal(h2.b.errors.length, 0, JSON.stringify(h2.b.errors[0]));
|
||||
checkInvariants(h.b);
|
||||
checkInvariants(h2.b);
|
||||
});
|
||||
Reference in new issue
Block a user