feedback5 O16: 自选 operator kits 涤火杰西卡 (+ her 机动盾牌), 机械师 (+ her 结构性原理) and 凯尔希 (+ her Mon3tr) — every skill, talent, trait and module (stage 1 / 3) at every form
- 涤火杰西卡 (ops/op-jesca2.js): the shield piece lasts 50 s (SPT-X stage 3: 60 s), holds 禁疗 and taunt 1, she turns to face it and back when it leaves, she and the unit behind her DEF +15 % (孤立 too), it falls with her and comes back 30 s later for 5 DP; T2 +1 SP per shield hit at 50 % (SPT-Y stage 3: 60 % and its redeploy −1 s per success, ≤ 14 s); S1 SP_FULL toggle ATK / DEF +40 / +55 % and the shield's DEF and +20 s life; S2 ACTIVE_RANGE 2-5 (not grown by 攻击距离), 0.3 s, 75 % dodge; S3 ACTIVE_RANGE 20 bullets, +1 range, 1.8 s, the shell (5 tiles / s, 1.7 burst, 250 % ATK, 5 s stun) while the shield stands next to her; SPT-X reveal, SPT-Y +1 range. - 机械师 (ops/op-mcnist.js): 结构性原理 30 s, 禁疗, back 40 s later for 10 DP; T2 30 % 屏障 (physical / arts, 超额防护); S1 3 bullets of 5 strikes (1.1, air too); S2 10 % 屏障 whose break bursts (200 % arts, 战栗 2 s) and buys a new one per bullet; S3 the 0.8 s cross (x-6, 240 % arts), the token's charge (300 % physical, redeployed on the stop tile) and its decaying weaken aura; SO-A: attributes only (its trait / talent are 集成战略-only). - 凯尔希 (ops/op-kalts.js): heal order herself / Mon3tr first (with no module the operator Mon3tr too), her heals through Mon3tr's 禁疗 (profile healThrough); Mon3tr DEF ×0 off her range (PHY-Y stage 3: ASPD +20 / DEF +20 % inside), withdrawn with her, back 25 s later for 10 DP; T2 1200 true + 3 s stun on its 3×3 when knocked out (PHY-X stage 3: 1500 / 3.5 s and the first time it falls to half HP); S1 DEF + 物理格挡; S2 / S3 bound to Mon3tr (绑定): Mon3tr hits every blocked enemy / true damage with an ATK bonus decaying every second and the 50 % 流失 without a kill; PHY-X / PHY-Y heal ×1.15; ISW-A attributes only. - Triggers from the data: the 重装 exception (S2 / S3 ACTIVE_RANGE for 涤火杰西卡, DEFAULT for 机械师 — rawRule TAKE_DAMAGE), 凯尔希's MANUAL skills DEFAULT (heal skills); 涤火杰西卡 S1 (AUTO, self / shield) SP_FULL by the owner's AUTO rule. - OPERATOR_KIT_FILES: op-jesca2.js, op-mcnist.js, op-kalts.js. Golden: only diy moved — diy-049 … diy-052 take the new pieces into their twelve, diy-053 … diy-058 are new (58 scenarios); roster 49, bonds 46, fields 22, matches 17, standins 10 unchanged. Tests: test/content/op_jesca2.test.js (10), op_mcnist.test.js (9), op_kalts.test.js (9).
This commit is contained in:
1 parent
5888f153ac
commit
d7a1d92d40
8 files changed
+2360
-117
No files matched your search
@@ -0,0 +1,349 @@
|
||||
// test/content/op_jesca2.test.js — the 自选 operator kit of 涤火杰西卡 (char_1034_jesca2, 6★ 哨戒铁卫; kit
|
||||
// server/sim/content/kits/ops/op-jesca2.js) and her summon 机动盾牌 (token_10032_jesca2_jckshd), fielded the production way (a
|
||||
// DIY slot + its `diy` pick; the shield as a token piece of hers) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4,
|
||||
// no module) and elite (E2 Lv60, rank 7) with no module, SPT-X “私家英雄” or SPT-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 token's variant); the fidelity
|
||||
// checklist of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_jesca2.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 JESCA = 'char_1034_jesca2';
|
||||
const SHIELD = 'token_10032_jesca2_jckshd';
|
||||
const FORMS = BACKUPS.units[JESCA].forms;
|
||||
const TOKEN = BACKUPS.tokens[SHIELD];
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const SPTX = 'uniequip_002_jesca2', SPTY = 'uniequip_003_jesca2';
|
||||
const S1 = 'skchr_jesca2_1', S2 = 'skchr_jesca2_2', S3 = 'skchr_jesca2_3';
|
||||
const statusKey = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusKey(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const variantOf = (tier, elite) => TOKEN.variants[`${JESCA}@${statusKey(tier, elite)}`];
|
||||
const tokenStats = (tier, elite, mod) => (elite && mod && variantOf(tier, elite).byModule?.[mod]?.stats) || variantOf(tier, elite).stats;
|
||||
const tokenTalent = (tier, elite, mod) => ((elite && mod && variantOf(tier, elite).byModule?.[mod]?.talents) || variantOf(tier, elite).talents).find((t) => t.bb.duration != null).bb;
|
||||
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: 200, range: 2, bat: 0.5 }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, SPTX, SPTY].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
const K = (r, c) => r * 21 + c;
|
||||
const sortN = (a) => [...a].sort((x, y) => x - y);
|
||||
/** Her shield above her (she turns UP), beside her (the tile right of her: in front, no turn), behind her (she turns LEFT). */
|
||||
const ABOVE = { row: 11, col: 4 }, FRONT = { row: 10, col: 5 }, BEHIND = { row: 10, col: 3 };
|
||||
|
||||
/** A battle with 涤火杰西卡 as uid 1 at (10, 4) facing RIGHT (`shield`: her piece as uid 2), plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, shield = ABOVE, others = [], seed = 5, dp = 99 } = {}) {
|
||||
const units = [{ uid: 1, diy: { slot: SLOT[tier], charId: JESCA, skillIndex: skill, uniEquipId: mod }, elite, row: 10, col: 4 }];
|
||||
if (shield) units.push({ uid: 2, kind: 'token', tokenId: SHIELD, ownerUid: 1, row: shield.row, col: shield.col });
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpInit: dp, dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'spGain', 'ammoUsed'], captureNoisy: true,
|
||||
units: [...units, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1), t: shield ? h.unit(2) : null };
|
||||
}
|
||||
const bombHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.tags?.includes('jesca2:bomb'));
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
|
||||
test('涤火杰西卡 in every 自选 form: her operator kit (all three skills authored), stats + module attributes, 2-2 (SPT-Y: +1), blocks 3, ranged physical, hits air, 维多利亚, no 特质; her 机动盾牌 piece: the form\'s token stats, 禁疗, taunt 1, no attack, blocks 2', () => {
|
||||
assert.equal(OPERATOR_KITS[JESCA], KITS[JESCA]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u, t } = field({ tier, elite, mod, skill, shield: FRONT });
|
||||
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], [JESCA, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [3, 'ranged', 'phys', true, 1.2], `${label(f)}: 哨戒铁卫`);
|
||||
const ext = mod === SPTY ? 1 : 0;
|
||||
assert.deepEqual(sortN(u.rangeKeys), sortN([...Array(3 + ext).keys()].map((i) => K(10, 4 + i))), `${label(f)}: 2-2${ext ? ' + 1' : ''}`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.tokens], [['victoriaShip'], [], [SHIELD]], `${label(f)}: bonds / 特质 / summon`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
const ts = tokenStats(tier, elite, mod);
|
||||
assert.deepEqual([t.base.maxHp, t.base.def, t.base.atk, t.s.blockCnt, t.base.cost, t.base.respawnTime], [ts.maxHp, ts.def, 0, 2, 5, 30], `${label(f)}: shield stats`);
|
||||
assert.deepEqual([t.s.flags.noHeal, t.s.taunt, t.profile.noAttack, t.alive], [true, tokenTalent(tier, elite, mod).taunt_level, true, true], `${label(f)}: 禁疗 / taunt / no attack`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// E2 Lv1 2589 / 433 / 522, E2 Lv60 3265 / 492 / 651; SPT-X +180 / +50 / +25 → +250 / +65 / +37, SPT-Y +180 / +52 → +325 / +83;
|
||||
// the shield 3041 / 619, 3545 / 709, SPT-X stage 3 3925 / 788 and 60 s
|
||||
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.def], [2589, 433, 3265, 651]);
|
||||
assert.deepEqual([modOf(5, SPTX).attr, modOf(6, SPTX).attr, modOf(5, SPTY).attr, modOf(6, SPTY).attr], [{ maxHp: 180, atk: 50, def: 25 }, { maxHp: 250, atk: 65, def: 37 }, { maxHp: 180, atk: 52 }, { maxHp: 325, atk: 83 }]);
|
||||
assert.deepEqual([tokenStats(5, false).maxHp, tokenStats(5, true).maxHp, tokenStats(6, true, SPTX).maxHp, tokenStats(6, true, SPTX).def, tokenTalent(6, true, SPTX).duration, tokenTalent(5, true, SPTX).duration], [3041, 3545, 3925, 788, 60, 50]);
|
||||
});
|
||||
|
||||
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(JESCA));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(JESCA), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: JESCA, skillIndex: 2, uniEquipId: SPTY } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: JESCA, skillIndex: 2, uniEquipId: SPTY } } });
|
||||
});
|
||||
|
||||
test('T1 灵活应变: she turns to face her shield (her range turns with her) and back once it leaves; while it stands next to her she and the unit on the tile behind her DEF +15 % (孤立 too), nobody else', () => {
|
||||
for (const f of [[5, false, null], [6, true, SPTX], [6, true, SPTY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
// shield above (11, 4): she faces UP; behind her is (9, 4); 德克萨斯 at (10, 3) was behind her RIGHT facing only
|
||||
const others = [{ uid: 3, chessId: 'chess_char_1_02_a', row: 9, col: 4 }, { uid: 4, chessId: 'chess_char_1_08_a', row: 10, col: 3 }];
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, shield: ABOVE, others });
|
||||
const yak = h.unit(3), texas = h.unit(4);
|
||||
const d = talentOf(tier, elite, mod, 0).def;
|
||||
assert.equal(d, 0.15);
|
||||
assert.deepEqual([u.dir, t.dir], ['UP', 'UP'], `${label(f)}: she faces her shield, it faces away from her`);
|
||||
const ext = mod === SPTY ? 1 : 0;
|
||||
assert.deepEqual(sortN(u.rangeKeys), sortN([...Array(3 + ext).keys()].map((i) => K(10 + i, 4))), `${label(f)}: her range turned UP`);
|
||||
assert.deepEqual(u.findBuff('talent:jesca2:def')?.mods, { defPct: d }, `${label(f)}: herself`);
|
||||
assert.deepEqual(yak.findBuff('talent:jesca2:def')?.mods, { defPct: d }, `${label(f)}: 角峰 behind her`);
|
||||
assert.equal(texas.findBuff('talent:jesca2:def'), null, `${label(f)}: 德克萨斯 beside her`);
|
||||
approx(u.s.def, u.base.def * (1 + d), `${label(f)}: her DEF`);
|
||||
// 孤立 does not keep it off (ignoreAllyTargetFree)
|
||||
h.b.addBuff(yak, { key: 'test:isolated', flags: { isolated: true } });
|
||||
h.run(0.5);
|
||||
assert.ok(yak.findBuff('talent:jesca2:def'), `${label(f)}: a 孤立 unit behind her`);
|
||||
// the shield leaves: she turns back to RIGHT, the aura ends
|
||||
h.b.retreat(t, { reason: 'expired', permanent: true });
|
||||
h.run(0.5);
|
||||
assert.deepEqual([u.dir, !!u.findBuff('talent:jesca2:def'), !!yak.findBuff('talent:jesca2:def')], ['RIGHT', false, false], `${label(f)}: back to RIGHT`);
|
||||
assert.deepEqual(sortN(u.rangeKeys), sortN([...Array(3 + ext).keys()].map((i) => K(10, 4 + i))), `${label(f)}: her range back`);
|
||||
done(h);
|
||||
}
|
||||
// behind her: she turns LEFT; in front: no turn; on a tile not next to her: no turn, no aura (the prep's placement rule)
|
||||
const b = field({ shield: BEHIND });
|
||||
assert.equal(b.u.dir, 'LEFT');
|
||||
assert.deepEqual(sortN(b.u.rangeKeys), sortN([K(10, 4), K(10, 3), K(10, 2)]), 'her range faces LEFT');
|
||||
done(b.h);
|
||||
const fr = field({ shield: FRONT });
|
||||
assert.deepEqual([fr.u.dir, !!fr.u.findBuff('talent:jesca2:def')], ['RIGHT', true]);
|
||||
done(fr.h);
|
||||
const far = field({ shield: { row: 12, col: 4 } });
|
||||
assert.deepEqual([far.u.dir, !!far.u.findBuff('talent:jesca2:def')], ['RIGHT', false], 'a shield two tiles away');
|
||||
done(far.h);
|
||||
});
|
||||
|
||||
test('T1 灵活应变: the shield lasts 50 s (SPT-X stage 3: 60 s), comes back on its tile 30 s after it left paying 5 DP — only while she stands; it falls with her and her redeployment readies it at once', () => {
|
||||
for (const f of [[5, false, null], [6, true, SPTX], [5, true, SPTX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, shield: ABOVE });
|
||||
const life = tokenTalent(tier, elite, mod).duration;
|
||||
assert.equal(life, tier === 6 && mod === SPTX ? 60 : 50, label(f));
|
||||
h.run(life - 0.6);
|
||||
assert.ok(t.alive, `${label(f)}: still there before ${life} s`);
|
||||
h.run(0.7);
|
||||
assert.ok(!t.alive && !t.removed, `${label(f)}: gone at ${life} s (the piece stays)`);
|
||||
assert.equal(u.dir, 'RIGHT');
|
||||
const dp0 = h.b.players[0].dp;
|
||||
h.run(29.5);
|
||||
assert.ok(!t.alive, `${label(f)}: not before 30 s`);
|
||||
h.run(0.7);
|
||||
assert.ok(t.alive, `${label(f)}: back after 30 s`);
|
||||
assert.deepEqual([t.tileR, t.tileC, u.dir], [11, 4, 'UP'], `${label(f)}: on its tile, she faces it again`);
|
||||
approx(h.b.players[0].dp, dp0 - 5, `${label(f)}: 5 DP`);
|
||||
// she falls ⇒ it falls; waiting while she is down; her redeployment brings it at once
|
||||
h.b.kill(u, null);
|
||||
h.step();
|
||||
assert.ok(!t.alive && !t.removed, `${label(f)}: falls with her`);
|
||||
assert.equal(u.dir, 'RIGHT', `${label(f)}: her facing restored for her redeployment`);
|
||||
h.run(40);
|
||||
assert.ok(!t.alive, `${label(f)}: no shield while she is down`);
|
||||
assert.ok(h.runUntil(() => u.alive, 60), `${label(f)}: she redeploys`);
|
||||
const back = h.b.time;
|
||||
assert.ok(h.runUntil(() => t.alive, 0.2), `${label(f)}: the shield with her (charge_token[born])`);
|
||||
assert.ok(h.b.time - back < 0.15, `${label(f)}: at once`);
|
||||
assert.equal(u.dir, 'UP');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 蓄能释放: each damage instance on the shield (next to her) gives her 1 SP with 50 % (SPT-Y stage 3: 60 % and its next redeploy −1 s per success, ≤ 14 s); a 流失 does not count', () => {
|
||||
for (const f of [[5, false, null], [6, true, SPTY], [5, true, SPTY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const tb = talentOf(tier, elite, mod, 1);
|
||||
const y3 = tier === 6 && mod === SPTY;
|
||||
assert.deepEqual([tb.prob, tb.sp, tb.respawn_time ?? 0, tb.respawn_time_max ?? 0], y3 ? [0.6, 1, 1, 14] : [0.5, 1, 0, 0], label(f));
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, shield: ABOVE, seed: 17 });
|
||||
// a shooter beside the shield and out of her range: it picks the shield (taunt 1), she cannot reach it
|
||||
h.spawn('enemy_shooter', { pos: [12, 5] });
|
||||
h.run(40);
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.target === t && c.amount > 0).length;
|
||||
const gains = h.hooksOf('spGain').filter((c) => c.unit === u && c.reason === 'talent');
|
||||
assert.ok(hits > 60, `${label(f)}: ${hits} hits`);
|
||||
assert.ok(gains.every((c) => c.amount === 1), `${label(f)}: 1 SP each`);
|
||||
const share = gains.length / hits;
|
||||
assert.ok(Math.abs(share - tb.prob) < 0.12, `${label(f)}: ${gains.length} / ${hits} ≈ ${tb.prob}`);
|
||||
assert.equal(t.mem.respawnCut, y3 ? Math.min(14, gains.length) : 0, `${label(f)}: redeploy cut`);
|
||||
const n = gains.length;
|
||||
h.b.loseHp(t, 10, { source: null });
|
||||
h.step();
|
||||
assert.equal(h.hooksOf('spGain').filter((c) => c.unit === u && c.reason === 'talent').length, n, `${label(f)}: a 流失 gives nothing`);
|
||||
if (y3) {
|
||||
h.b.retreat(t, { reason: 'expired', permanent: true });
|
||||
approx(t.mem.readyAt, h.b.time + 30 - 14, `${label(f)}: back 30 − 14 s later`, 1e-6);
|
||||
}
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 坚守阵线 (AUTO, SP_FULL): at full SP with no enemy, 持续时间无限; ATK / DEF +40 % / +55 %; her shield DEF +40 % / +55 % and +20 s of life once per deployment', () => {
|
||||
for (const f of [[5, false, null], [6, true, SPTX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0, shield: ABOVE });
|
||||
assert.deepEqual([u.skill.rule, sk.trigger.rule, u.skill.kind, sk.spCost, sk.bb.atk, sk.bb.def, sk.bb.duration], ['SP_FULL', 'DEFAULT', 'toggle', elite ? 90 : 100, elite ? 0.55 : 0.4, elite ? 0.55 : 0.4, 20], label(f));
|
||||
const life = tokenTalent(tier, elite, mod).duration;
|
||||
assert.ok(h.runUntil(() => u.skill.active, sk.spCost + 1), `${label(f)}: cast at ${sk.spCost} s with no enemy`);
|
||||
approx(h.b.time, sk.spCost, `${label(f)}: at full SP`, 0.02);
|
||||
// the first shield expired before (life < cost) and came back at life + 30: it gets the 20 s now
|
||||
assert.ok(t.alive);
|
||||
h.step();
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
|
||||
approx(u.s.def, u.base.def * (1 + sk.bb.def + 0.15), `${label(f)}: DEF (+15 % T1)`);
|
||||
approx(t.s.def, t.base.def * (1 + sk.bb.def), `${label(f)}: shield DEF`);
|
||||
approx(t.mem.lifeEnd - t.deployedAt, life + 20, `${label(f)}: shield life +20 s`);
|
||||
h.run(200);
|
||||
assert.ok(u.skill.active, `${label(f)}: still on 200 s later`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 掩蔽护卫 (MANUAL, data ACTIVE_RANGE — rawRule TAKE_DAMAGE): casts with an enemy on its 2-5 that her 2-2 does not reach; 15 s, range 2-5 (SPT-Y does not grow it), ATK +45 % / +60 %, interval 0.3 s, 75 % physical and arts dodge', () => {
|
||||
for (const f of [[5, false, null], [6, true, SPTY], [5, true, SPTY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, shield: null });
|
||||
assert.deepEqual([u.skill.rule, sk.trigger.rule, sk.trigger.rawRule, sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.bb.prob], ['ACTIVE_RANGE', 'ACTIVE_RANGE', 'TAKE_DAMAGE', 15, elite ? 0.6 : 0.45, -0.9, 0.75], label(f));
|
||||
u.skill.gainSp(999);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, `${label(f)}: no enemy, no cast`);
|
||||
// (11, 6): on the 2-5 (local [1, 2]), not on her 2-2 (+1)
|
||||
h.spawn('enemy_dummy', { pos: [11, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `${label(f)}: cast for an enemy only the skill range reaches`);
|
||||
approx(u.skill.timeLeft, 15, label(f), 0.01);
|
||||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `${label(f)}: 2-5 exactly`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
|
||||
approx(u.s.interval, 0.3, `${label(f)}: 1.2 − 0.9 s`);
|
||||
assert.deepEqual([u.s.dodgePhys, u.s.dodgeArts], [0.75, 0.75], `${label(f)}: dodge`);
|
||||
h.runUntil(() => !u.skill.active, 20);
|
||||
const ext = mod === SPTY ? 1 : 0;
|
||||
assert.deepEqual(sortN(u.rangeKeys), sortN([...Array(3 + ext).keys()].map((i) => K(10, 4 + i))), `${label(f)}: back to her own range`);
|
||||
assert.deepEqual([u.s.dodgePhys, u.s.interval], [0, 1.2], `${label(f)}: all back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 饱和迸射 (MANUAL, data ACTIVE_RANGE on 2-2 + 1): 20 bullets, range +1 (SPT-Y: +2), interval 1.8 s, ATK +240 % / +270 %, DEF +50 % / +65 %, shield DEF +100 % / +135 %; casts with an enemy only the larger range reaches', () => {
|
||||
for (const f of [[5, false, null], [6, true, SPTY], [6, true, SPTX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 2, shield: BEHIND });
|
||||
assert.deepEqual([u.skill.rule, sk.trigger.rule, sk.trigger.rawRule, u.skill.kind, u.skill.ammo], ['ACTIVE_RANGE', 'ACTIVE_RANGE', 'TAKE_DAMAGE', 'ammo', 20], label(f));
|
||||
assert.equal(u.dir, 'LEFT');
|
||||
const ext = mod === SPTY ? 1 : 0;
|
||||
u.skill.gainSp(999);
|
||||
// facing LEFT from (10, 4): her own range reaches col 4 − 2 − ext; one more tile only with the skill
|
||||
h.spawn('enemy_dummy', { pos: [10, 1 - ext] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 1), `${label(f)}: cast`);
|
||||
assert.deepEqual(sortN(u.rangeKeys), sortN([...Array(4 + ext).keys()].map((i) => K(10, 4 - i))), `${label(f)}: +1 tile (${ext ? 'and SPT-Y\'s' : 'no module'})`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
|
||||
approx(u.s.def, u.base.def * (1 + sk.bb['jesca2_s_3[def].def'] + 0.15), `${label(f)}: DEF (+15 % T1)`);
|
||||
approx(u.s.interval, 1.8, `${label(f)}: 1.2 + 0.6 s`);
|
||||
approx(t.s.def, t.base.def * (1 + sk.bb['jesca2_s_3_token[def].def']), `${label(f)}: shield DEF`);
|
||||
assert.deepEqual([sk.bb.atk, sk.bb['jesca2_s_3[def].def'], sk.bb['jesca2_s_3_token[def].def']], elite ? [2.7, 0.65, 1.35] : [2.4, 0.5, 1], label(f));
|
||||
h.runUntil(() => !u.skill.active, 60);
|
||||
assert.equal(h.hooksOf('ammoUsed').filter((c) => c.unit === u).length, 20, `${label(f)}: 20 bullets`);
|
||||
assert.equal(t.findBuff('skill:jesca2:shield'), null, `${label(f)}: shield DEF back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 shell: with her shield next to her at the cast it flies ahead at 5 tiles / s, bursts on the first enemy it touches: every enemy within 1.7 (air units too) takes 250 % ATK physical and a 5 s stun; no bullet spent; none without a shield, one when a shield comes during the skill, held while she is disarmed', () => {
|
||||
for (const f of [[5, false, null], [6, true, SPTY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.bb['attack@extrabomb.atk_scale'], sk.bb['attack@extrabomb.stun'], sk.bb['attack@extrabomb.projectile_range']], [2.5, 5, 1.7], label(f));
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 2, shield: FRONT });
|
||||
const near = h.spawn('enemy_dummy', { pos: [10, 7] }), side = h.spawn('enemy_dummy', { pos: [11, 7] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [11, 6] }), far = h.spawn('enemy_dummy', { pos: [12, 8] });
|
||||
u.skill.gainSp(999);
|
||||
u.skill.activate('test');
|
||||
const atk = u.s.atk;
|
||||
assert.ok(h.runUntil(() => bombHits(h, u).length > 0, 2), `${label(f)}: the shell lands`);
|
||||
// from (10, 4) to the enemy at (10, 7): it touches it 0.5 before, at x 6.5, i.e. 2.5 tiles in 0.5 s
|
||||
approx(h.hooksOf('damaged').find((c) => c.dmg?.tags?.includes('jesca2:bomb')).t, 0.5 + h.hooksOf('skillStart').find((c) => c.unit === u).t, `${label(f)}: flight time`, 0.1);
|
||||
const hits = bombHits(h, u);
|
||||
assert.deepEqual(sortN(hits.map((c) => c.target.id)), sortN([near.id, side.id, fly.id]), `${label(f)}: within 1.7, the flyer too`);
|
||||
for (const c of hits) {
|
||||
approx(c.amount, atk * 2.5, `${label(f)}: 250 % ATK`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true], label(f));
|
||||
}
|
||||
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun');
|
||||
assert.deepEqual(sortN(stuns.map((c) => c.target.id)), sortN([near.id, side.id, fly.id]), `${label(f)}: stunned`);
|
||||
for (const c of stuns) approx(c.duration, 5, `${label(f)}: 5 s`);
|
||||
assert.ok(!far.findBuff('stun'), `${label(f)}: out of the burst`);
|
||||
assert.equal(u.skill.ammoLeft + h.hooksOf('ammoUsed').filter((c) => c.unit === u).length, 20, `${label(f)}: the shell spends no bullet`);
|
||||
h.run(3);
|
||||
assert.equal(bombHits(h, u).length, hits.length, `${label(f)}: one shell per cast`);
|
||||
// a shield coming during the skill fires another (her shield back at once)
|
||||
h.b.retreat(t, { reason: 'expired', permanent: true });
|
||||
t.mem.readyAt = h.b.time;
|
||||
assert.ok(h.runUntil(() => t.alive, 1), label(f));
|
||||
assert.ok(h.runUntil(() => bombHits(h, u).length > hits.length, 2), `${label(f)}: a second shell`);
|
||||
done(h);
|
||||
}
|
||||
// no shield: no shell; disarmed: it waits
|
||||
const a = field({ skill: 2, shield: null });
|
||||
a.h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
a.u.skill.gainSp(999);
|
||||
a.u.skill.activate('test');
|
||||
a.h.run(3);
|
||||
assert.equal(bombHits(a.h, a.u).length, 0, 'no shield: no shell');
|
||||
done(a.h);
|
||||
const b = field({ skill: 2, shield: FRONT });
|
||||
b.h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
b.h.b.addBuff(b.u, { key: 'test:disarm', flags: { disarm: true }, duration: 2 });
|
||||
b.u.skill.gainSp(999);
|
||||
b.u.skill.activate('test');
|
||||
b.h.run(1.5);
|
||||
assert.equal(bombHits(b.h, b.u).length, 0, 'disarmed: held');
|
||||
assert.ok(b.h.runUntil(() => bombHits(b.h, b.u).length > 0, 2), 'fired once the disarm is gone');
|
||||
done(b.h);
|
||||
});
|
||||
|
||||
test('modules: SPT-X reveals the stealthed enemies of her range (stages 1 and 3); SPT-Y is a permanent +1 range (her initial range, S3 on top, never S2); with no module neither', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, shield: null });
|
||||
const inR = h.spawn('enemy_dummy', { pos: [10, 6] }), outR = h.spawn('enemy_dummy', { pos: [10, 9] });
|
||||
for (const e of [inR, outR]) h.b.addBuff(e, { key: 'test:stealth', flags: { stealth: true } });
|
||||
h.run(0.5);
|
||||
assert.equal(!!inR.s.flags.reveal, elite && mod === SPTX, `${label(f)}: reveal in range`);
|
||||
assert.equal(!!outR.s.flags.reveal, false, `${label(f)}: never out of range`);
|
||||
const ext = elite && mod === SPTY ? 1 : 0;
|
||||
assert.equal(u.s.baseRangeExtend, ext, `${label(f)}: permanent extension`);
|
||||
assert.deepEqual(u.liveRangeGrid.length, 3 + ext, `${label(f)}: initial range`);
|
||||
if (elite && mod) {
|
||||
const m = modOf(tier, mod);
|
||||
assert.ok(mod === SPTX ? /隐匿/.test(m.traitOverride.moduleDesc) : m.traitOverride.bb.ability_range_forward_extend === 1, `${label(f)}: the data`);
|
||||
}
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,355 @@
|
||||
// test/content/op_kalts.test.js — the 自选 operator kit of 凯尔希 (char_003_kalts, 6★ 医师; kit
|
||||
// server/sim/content/kits/ops/op-kalts.js) and her summon Mon3tr (token_10002_kalts_mon3tr), fielded the production way (a
|
||||
// DIY slot + its `diy` pick; Mon3tr as a token piece of hers) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no
|
||||
// module) and elite (E2 Lv60, rank 7) with no module, PHY-X Mon2tr, PHY-Y 医者 or ISW-A 凯尔希特限证章 at stage 1 (tier 5) /
|
||||
// 3 (tier 6). Every number is read back from data/backups.json (the form of that slot status, the token's variant); the
|
||||
// fidelity checklist of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_kalts.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 KALTS = 'char_003_kalts';
|
||||
const MON3TR = 'token_10002_kalts_mon3tr';
|
||||
const FORMS = BACKUPS.units[KALTS].forms;
|
||||
const TOKEN = BACKUPS.tokens[MON3TR];
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const PHYX = 'uniequip_002_kalts', PHYY = 'uniequip_003_kalts', ISWA = 'uniequip_004_kalts';
|
||||
const S1 = 'skchr_kalts_1', S2 = 'skchr_kalts_2', S3 = 'skchr_kalts_3';
|
||||
const statusKey = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusKey(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const variantOf = (tier, elite) => TOKEN.variants[`${KALTS}@${statusKey(tier, elite)}`];
|
||||
const rattleOf = (tier, elite, mod) => ((elite && mod && variantOf(tier, elite).byModule?.[mod]?.talents) || variantOf(tier, elite).talents).find((t) => t.bb.value != null).bb;
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = {
|
||||
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
|
||||
enemy_biter: dummy('enemy_biter', { atk: 600, range: 1.2, bat: 1 }),
|
||||
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0, atk: 200, bat: 1 }),
|
||||
enemy_weak: enemyRec({ key: 'enemy_weak', hp: 50, speed: 0, mass: 0 }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, PHYX, PHYY, ISWA].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** A battle with 凯尔希 as uid 1 at (10, 2) facing RIGHT (`mon3tr`: her piece as uid 2), plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, mon3tr = { row: 10, col: 4 }, others = [], seed = 5, dp = 99 } = {}) {
|
||||
const units = [{ uid: 1, diy: { slot: SLOT[tier], charId: KALTS, skillIndex: skill, uniEquipId: mod }, elite, row: 10, col: 2 }];
|
||||
if (mon3tr) units.push({ uid: 2, kind: 'token', tokenId: MON3TR, ownerUid: 1, row: mon3tr.row, col: mon3tr.col });
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpInit: dp, dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'heal', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'kill'], captureNoisy: true,
|
||||
units: [...units, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1), m: mon3tr ? h.unit(2) : null };
|
||||
}
|
||||
const heals = (h, u) => h.hooksOf('heal').filter((c) => c.source === u && !c.opts?.regen);
|
||||
const rattles = (h, m) => h.hooksOf('damaged').filter((c) => c.dmg?.tags?.includes('kalts:rattle') && (c.source === m || c.credit === m));
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
|
||||
test('凯尔希 in every 自选 form: her operator kit (all three skills authored), stats + module attributes, 3-10, one heal per attack, blocks 1, 罗德岛, no 特质; her Mon3tr: the form\'s token stats, 禁疗, 1-1, blocks 3, melee ground-only physical', () => {
|
||||
assert.equal(OPERATOR_KITS[KALTS], KITS[KALTS]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u, m } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), md = elite ? modOf(tier, mod) : null;
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [KALTS, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd],
|
||||
[form.stats.maxHp + (md?.attr.maxHp ?? 0), form.stats.atk + (md?.attr.atk ?? 0), form.stats.def + (md?.attr.def ?? 0), form.stats.aspd + (md?.attr.aspd ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.profile.dmgType, u.profile.heal, u.s.blockCnt, u.base.bat], ['heal', { mode: 'single' }, 1, 2.85], `${label(f)}: 医师`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-10`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.tokens], [BACKUPS.diy.operators[KALTS].bonds, [], [MON3TR]], `${label(f)}: bonds / 特质 / summon`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: enemies target her`);
|
||||
const ts = variantOf(tier, elite).stats;
|
||||
assert.deepEqual([m.base.maxHp, m.base.atk, m.base.def, m.s.blockCnt, m.base.bat, m.base.cost, m.base.respawnTime], [ts.maxHp, ts.atk, ts.def, 3, 2, 10, 25], `${label(f)}: Mon3tr stats`);
|
||||
assert.deepEqual([m.s.flags.noHeal, m.profile.attack, m.profile.dmgType, m.profile.canHitFly, m.liveRangeGrid], [true, 'melee', 'phys', false, [[0, 0], [0, 1]]], `${label(f)}: Mon3tr 禁疗 / melee ground-only`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// E2 Lv1 1469 / 392 / 172, E2 Lv60 1578 / 457 / 201; PHY-X +35 / +15 → +60 / +25 ATK / DEF, PHY-Y +115 / +5 → +195 / +7
|
||||
// HP / ASPD, ISW-A +24 / +5 → +50 / +7 ATK / ASPD; Mon3tr 4292 / 1149 / 336 → 5048 / 1317 / 382
|
||||
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], [1469, 392, 1578, 457]);
|
||||
assert.deepEqual([modOf(5, PHYX).attr, modOf(6, PHYX).attr, modOf(5, PHYY).attr, modOf(6, PHYY).attr, modOf(5, ISWA).attr, modOf(6, ISWA).attr],
|
||||
[{ atk: 35, def: 15 }, { atk: 60, def: 25 }, { maxHp: 115, aspd: 5 }, { maxHp: 195, aspd: 7 }, { atk: 24, aspd: 5 }, { atk: 50, aspd: 7 }]);
|
||||
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(5, false).stats.atk, variantOf(6, true).stats.maxHp], [4292, 1149, 5048]);
|
||||
});
|
||||
|
||||
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(KALTS));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(KALTS), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: KALTS, skillIndex: 1, uniEquipId: PHYX } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: KALTS, skillIndex: 1, uniEquipId: PHYX } } });
|
||||
});
|
||||
|
||||
test('T1 heal order: herself and her Mon3tr first when injured (the lower HP ratio; ties: herself), else the lowest ratio; her heals pass Mon3tr\'s 禁疗, another medic never heals it; with no module an operator Mon3tr is first too', () => {
|
||||
const others = [{ uid: 3, chessId: 'chess_char_1_02_a', row: 9, col: 5 }, { uid: 4, chessId: 'chess_char_2_02_a', row: 11, col: 3 }];
|
||||
const { h, u, m } = field({ skill: 0, others });
|
||||
const yak = h.unit(3), silent = h.unit(4);
|
||||
const at = (x, r) => { x.hp = x.s.maxHp * r; };
|
||||
at(yak, 0.3); at(m, 0.6);
|
||||
const n0 = heals(h, u).length;
|
||||
assert.ok(h.runUntil(() => heals(h, u).length > n0, 4), 'a heal');
|
||||
assert.equal(heals(h, u)[n0].target, m, 'Mon3tr (60 %) before 角峰 (30 %)');
|
||||
approx(heals(h, u)[n0].amount, u.s.atk, 'a full heal through its 禁疗');
|
||||
assert.equal(h.hooksOf('heal').filter((c) => c.source === silent && c.target === m).length, 0, '赫默 never heals Mon3tr');
|
||||
// herself at the same ratio: herself
|
||||
at(m, 0.5); at(u, 0.5);
|
||||
const n1 = heals(h, u).length;
|
||||
assert.ok(h.runUntil(() => heals(h, u).length > n1, 4));
|
||||
assert.equal(heals(h, u)[n1].target, u, 'a tie: herself');
|
||||
// both full: the lowest ratio of the rest
|
||||
at(m, 1); at(u, 1); at(yak, 0.2);
|
||||
const n2 = heals(h, u).length;
|
||||
assert.ok(h.runUntil(() => heals(h, u).length > n2, 4));
|
||||
assert.equal(heals(h, u)[n2].target, yak, 'then 角峰');
|
||||
done(h);
|
||||
// the operator Mon3tr (char_4179_monstr): first with no module, an ordinary target with one
|
||||
for (const [elite, mod, first] of [[false, null, true], [true, PHYY, false]]) {
|
||||
const opM = [{ uid: 3, diy: { slot: 'chess_char_6_diy2_a', charId: 'char_4179_monstr', skillIndex: 0 }, row: 9, col: 4, dir: 'LEFT' },
|
||||
{ uid: 4, chessId: 'chess_char_1_02_a', row: 11, col: 4 }];
|
||||
const r = field({ tier: 6, elite, mod, skill: 0, mon3tr: null, others: opM });
|
||||
const om = r.h.unit(3), y = r.h.unit(4);
|
||||
assert.equal(om.def.charId, 'char_4179_monstr');
|
||||
om.hp = om.s.maxHp * 0.7; y.hp = y.s.maxHp * 0.4;
|
||||
const k0 = heals(r.h, r.u).length;
|
||||
assert.ok(r.h.runUntil(() => heals(r.h, r.u).length > k0, 4));
|
||||
assert.equal(heals(r.h, r.u)[k0].target, first ? om : y, `${mod ?? 'no module'}: ${first ? 'the operator Mon3tr' : '角峰'} first`);
|
||||
done(r.h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 Mon3tr: DEF ×0 off her range (PHY-Y stage 3: inside ASPD +20, DEF +20 %); withdrawn when she leaves (no blast), back 25 s after it left paying 10 DP, only while she stands; her redeployment readies it at once', () => {
|
||||
for (const f of [[5, false, null], [6, true, PHYY], [5, true, PHYY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = (elite && mod && modOf(tier, mod).talentChanges.find((x) => x.talentIndex === 0)?.bb) || formOf(tier, elite).talents.find((x) => x.index === 0).bb;
|
||||
const y3 = tier === 6 && mod === PHYY;
|
||||
assert.deepEqual([t0.def, t0.attack_speed], y3 ? [0.2, 20] : [0, 0], label(f));
|
||||
const { h, u, m } = field({ tier, elite, mod, skill: 0 });
|
||||
approx(m.s.def, m.base.def * (1 + t0.def), `${label(f)}: in her range`);
|
||||
approx(m.s.aspd, m.base.aspd + t0.attack_speed, `${label(f)}: ASPD`);
|
||||
const off = field({ tier, elite, mod, skill: 0, mon3tr: { row: 10, col: 1 } }); // behind her: off her 3-10
|
||||
assert.equal(off.m.s.def, 0, `${label(f)}: DEF 0 off her range`);
|
||||
approx(off.m.s.aspd, off.m.base.aspd, `${label(f)}: no bonus off her range`);
|
||||
done(off.h);
|
||||
// she leaves: it is withdrawn (no 不毁重构), then waits for her
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.b.kill(u, null);
|
||||
h.step();
|
||||
assert.ok(!m.alive && !m.removed, `${label(f)}: withdrawn with her`);
|
||||
assert.equal(h.hooksOf('death').find((c) => c.unit === m)?.reason, 'retreat', label(f));
|
||||
assert.equal(rattles(h, m).length, 0, `${label(f)}: no blast on a withdrawal`);
|
||||
assert.ok(h.runUntil(() => u.alive, 90), `${label(f)}: she redeploys`);
|
||||
const back = h.b.time;
|
||||
assert.ok(h.runUntil(() => m.alive, 0.2), `${label(f)}: Mon3tr at once`);
|
||||
assert.ok(h.b.time - back < 0.15, label(f));
|
||||
// killed: back 25 s later paying 10 DP
|
||||
const dp0 = h.b.players[0].dp;
|
||||
h.b.kill(m, null);
|
||||
h.run(24.5);
|
||||
assert.ok(!m.alive, `${label(f)}: not before 25 s`);
|
||||
h.run(0.7);
|
||||
assert.ok(m.alive, `${label(f)}: back`);
|
||||
approx(h.b.players[0].dp, dp0 - 10, `${label(f)}: 10 DP`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 不毁重构: Mon3tr knocked out ⇒ every enemy of its 3×3 (air units too) takes 1200 true damage (溅射) and a 3 s stun; PHY-X stage 3: 1500 / 3.5 s, and once per deployment when a hit leaves it at or below half HP', () => {
|
||||
for (const f of [[5, false, null], [6, true, PHYX], [5, true, PHYX], [6, true, PHYY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const rb = rattleOf(tier, elite, mod);
|
||||
const x3 = tier === 6 && mod === PHYX;
|
||||
assert.deepEqual([rb.value, rb.stun, rb.hp_ratio ?? 0], x3 ? [1500, 3.5, 0.5] : [1200, 3, 0], label(f));
|
||||
const { h, m } = field({ tier, elite, mod, skill: 0 });
|
||||
const e1 = h.spawn('enemy_dummy', { pos: [11, 5] }), e2 = h.spawn('enemy_fly', { pos: [9, 3] }), far = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.step(); // the enemies on the tile index
|
||||
if (x3) {
|
||||
// a hit to 40 %: the half-HP blast, once
|
||||
h.b.dealDamage(null, m, { amount: m.s.maxHp * 0.6, type: 'true' });
|
||||
assert.equal(rattles(h, m).length, 2, `${label(f)}: the half-HP blast`);
|
||||
h.b.dealDamage(null, m, { amount: 1, type: 'true' });
|
||||
assert.equal(rattles(h, m).length, 2, `${label(f)}: once per deployment`);
|
||||
}
|
||||
const n0 = rattles(h, m).length;
|
||||
h.b.dealDamage(null, m, { amount: 1e9, type: 'true' });
|
||||
assert.ok(!m.alive, label(f));
|
||||
const r = rattles(h, m).slice(n0);
|
||||
assert.deepEqual(r.map((c) => c.target.id).sort(), [e1.id, e2.id].sort(), `${label(f)}: its 3×3, the flyer too`);
|
||||
for (const c of r) { approx(c.amount, rb.value, `${label(f)}: ${rb.value} true`); assert.deepEqual([c.type, c.dmg.isSplash], ['true', true]); }
|
||||
const st = h.hooksOf('statusApplied').filter((c) => c.status === 'stun' && c.target !== m);
|
||||
assert.ok(st.length >= 2 && st.every((c) => Math.abs(c.duration - rb.stun) < 1e-6), `${label(f)}: ${rb.stun} s`);
|
||||
assert.ok(!far.findBuff('stun'), `${label(f)}: two tiles away`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 指令:结构加固 (MANUAL, data DEFAULT — cast as she is about to heal): 33 / 36 s, her DEF +90 % / +120 % and Mon3tr\'s; 物理格挡 30 % / 40 %: a physical instance blocked whole (arts never)', () => {
|
||||
for (const f of [[5, false, null], [6, true, PHYX]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.trigger.rule, sk.duration, sk.bb.def, sk.bb['attack@def'], sk.bb.prob], ['DEFAULT', elite ? 36 : 33, elite ? 1.2 : 0.9, elite ? 1.2 : 0.9, elite ? 0.4 : 0.3], label(f));
|
||||
const { h, u, m } = field({ tier, elite, mod, skill: 0, seed: 23 });
|
||||
u.skill.gainSp(999);
|
||||
h.run(3);
|
||||
assert.equal(u.skill.activations, 0, `${label(f)}: nobody injured, no cast`);
|
||||
m.hp = m.s.maxHp * 0.5;
|
||||
assert.ok(h.runUntil(() => u.skill.active, 4), `${label(f)}: cast on her heal`);
|
||||
approx(u.skill.timeLeft, sk.duration, label(f), 0.01);
|
||||
approx(u.s.def, u.base.def * (1 + sk.bb.def), `${label(f)}: her DEF`);
|
||||
h.step();
|
||||
approx(m.s.def, m.base.def * (1 + sk.bb['attack@def']), `${label(f)}: Mon3tr DEF`);
|
||||
let blocked = 0;
|
||||
const N = 400;
|
||||
for (let i = 0; i < N; i++) {
|
||||
if (h.b.dealDamage(null, u, { amount: 1000, type: 'phys', canDodge: false }) === 0) blocked++;
|
||||
u.hp = u.s.maxHp;
|
||||
}
|
||||
assert.ok(Math.abs(blocked / N - sk.bb.prob) < 0.07, `${label(f)}: ${blocked} / ${N} ≈ ${sk.bb.prob}`);
|
||||
let artsBlocked = 0;
|
||||
for (let i = 0; i < 100; i++) { if (h.b.dealDamage(null, u, { amount: 10, type: 'arts', canDodge: false }) === 0) artsBlocked++; u.hp = u.s.maxHp; }
|
||||
assert.equal(artsBlocked, 0, `${label(f)}: arts never`);
|
||||
h.runUntil(() => !u.skill.active, 40);
|
||||
h.step();
|
||||
approx(m.s.def, m.base.def, `${label(f)}: Mon3tr DEF back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 指令:战术协同 (MANUAL, data DEFAULT, 绑定 Mon3tr): her ASPD +45 / +60, Mon3tr ATK +35 % / +50 % and each attack strikes every enemy it blocks; no Mon3tr: no cast, SP emptied and frozen; it leaving ends the skill', () => {
|
||||
for (const f of [[5, false, null], [6, true, PHYY]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.trigger.rule, sk.duration, sk.bb.attack_speed, sk.bb['attack@atk']], ['DEFAULT', elite ? 17 : 16, elite ? 60 : 45, elite ? 0.5 : 0.35], label(f));
|
||||
const { h, u, m } = field({ tier, elite, mod, skill: 1, mon3tr: { row: 9, col: 5 } });
|
||||
const w1 = h.spawn('enemy_walk', { pos: [9, 6], routeIndex: 0 }), w2 = h.spawn('enemy_walk', { pos: [9, 7], routeIndex: 0 });
|
||||
assert.ok(h.runUntil(() => m.blocking.length === 2, 20), `${label(f)}: Mon3tr blocks both`);
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 6), `${label(f)}: cast on a heal of Mon3tr`);
|
||||
approx(u.s.aspd, u.base.aspd + sk.bb.attack_speed, `${label(f)}: her ASPD`);
|
||||
h.step();
|
||||
approx(m.s.atk, m.base.atk * (1 + sk.bb['attack@atk']), `${label(f)}: Mon3tr ATK`);
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
h.run(4.5);
|
||||
const hits = h.hooksOf('damaged').slice(n0).filter((c) => c.source === m && c.dmg?.isAttack);
|
||||
const ids = new Map();
|
||||
for (const c of hits) ids.set(c.dmg.attackId, (ids.get(c.dmg.attackId) ?? new Set()).add(c.target.id));
|
||||
assert.ok(ids.size >= 2 && [...ids.values()].every((s) => s.size === 2 && s.has(w1.id) && s.has(w2.id)), `${label(f)}: every attack on both`);
|
||||
// Mon3tr killed: the skill ends within 0.1 s, the SP is emptied and recovers no more until it is back
|
||||
h.b.kill(m, null);
|
||||
h.run(0.15);
|
||||
assert.ok(!u.skill.active, `${label(f)}: ended (绑定)`);
|
||||
assert.equal(u.skill.sp, 0, label(f));
|
||||
h.run(5);
|
||||
assert.equal(u.skill.sp, 0, `${label(f)}: no SP without Mon3tr`);
|
||||
assert.ok(u.findBuff('kalts:binding'), label(f));
|
||||
m.mem.readyAt = h.b.time;
|
||||
assert.ok(h.runUntil(() => m.alive, 1), label(f));
|
||||
h.run(2);
|
||||
assert.ok(u.skill.sp > 1.5, `${label(f)}: SP again with Mon3tr back`);
|
||||
done(h);
|
||||
}
|
||||
// no Mon3tr piece at all: S2 never gains SP, never casts
|
||||
const { h, u } = field({ skill: 1, mon3tr: null, others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 10, col: 4 }] });
|
||||
h.unit(3).hp = 100;
|
||||
h.run(30);
|
||||
assert.deepEqual([u.skill.sp, u.skill.activations], [0, 0], 'no Mon3tr: no SP');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 指令:熔毁 (MANUAL, data DEFAULT, 绑定 Mon3tr, 18 s): Mon3tr DEF +120 % / +140 %, ATK +160 % / +190 % down by 1/18 every second, true damage; no kill ⇒ 50 % max HP 流失 at the end (it may knock it out: 不毁重构); a kill ⇒ none', () => {
|
||||
for (const f of [[5, false, null], [6, true, ISWA]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.trigger.rule, sk.duration, sk.bb['attack@atk'], sk.bb['attack@def'], sk.bb['attack@hp_ratio']], ['DEFAULT', 18, elite ? 1.9 : 1.6, elite ? 1.4 : 1.2, 0.5], label(f));
|
||||
const { h, u, m } = field({ tier, elite, mod, skill: 2 });
|
||||
const e = h.spawn('enemy_biter', { pos: [10, 5] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 6), `${label(f)}: cast`);
|
||||
const cast = h.hooksOf('skillStart').find((c) => c.unit === u).t;
|
||||
h.step();
|
||||
approx(m.s.def, m.base.def * (1 + sk.bb['attack@def']), `${label(f)}: DEF`);
|
||||
approx(m.s.atk, m.base.atk * (1 + sk.bb['attack@atk']), `${label(f)}: ATK at the cast`);
|
||||
h.run(cast + 5.5 - h.b.time);
|
||||
approx(m.s.atk, m.base.atk * (1 + sk.bb['attack@atk'] * (1 - 5 / 18)), `${label(f)}: 5 s on`);
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === m && c.target === e && c.dmg?.isAttack && c.t >= cast);
|
||||
assert.ok(hits.length > 0 && hits.every((c) => c.type === 'true'), `${label(f)}: true damage`);
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 15), label(f));
|
||||
assert.equal(h.hooksOf('skillEnd').find((c) => c.unit === u)?.reason, 'duration', label(f));
|
||||
const loss = h.hooksOf('damaged').filter((c) => c.target === m && c.dmg?.tags?.includes('kalts:meltdown'));
|
||||
assert.equal(loss.length, 1, `${label(f)}: the 流失 (it killed nothing: the biter has 1e9 HP)`);
|
||||
approx(loss[0].amount, m.s.maxHp * sk.bb['attack@hp_ratio'], `${label(f)}: 50 % max HP`);
|
||||
assert.ok(loss[0].dmg.tags.includes('hpLoss'), `${label(f)}: a 流失`);
|
||||
h.step();
|
||||
approx(m.s.atk, m.base.atk, `${label(f)}: ATK back`);
|
||||
done(h);
|
||||
}
|
||||
// the loss knocks it out: 不毁重构
|
||||
{
|
||||
const { h, u, m } = field({ skill: 2 });
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
const near = h.spawn('enemy_dummy', { pos: [11, 4] });
|
||||
m.hp = m.s.maxHp * 0.3;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 6));
|
||||
// her heals keep it up meanwhile: set it low just before the end
|
||||
assert.ok(h.runUntil(() => u.skill.timeLeft < 0.05, 20));
|
||||
m.hp = m.s.maxHp * 0.3;
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 1));
|
||||
assert.ok(!m.alive, 'knocked out by the 流失');
|
||||
assert.equal(h.hooksOf('death').find((c) => c.unit === m)?.reason, 'killed');
|
||||
assert.ok(rattles(h, m).some((c) => c.target === near), '不毁重构');
|
||||
done(h);
|
||||
}
|
||||
// a kill keeps its HP
|
||||
{
|
||||
const { h, u, m } = field({ skill: 2 });
|
||||
h.spawn('enemy_weak', { pos: [10, 5] });
|
||||
m.hp = m.s.maxHp * 0.9;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 6));
|
||||
assert.ok(h.runUntil(() => h.hooksOf('kill').some((c) => c.killer === m), 10), 'Mon3tr kills');
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 20));
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.target === m && c.dmg?.tags?.includes('kalts:meltdown')).length, 0, 'no 流失 after a kill');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('modules: PHY-X ×1.15 on a heal of a target at or below 50 %; PHY-Y ×1.15 on a heal of a target on a 地面 tile; ISW-A (集成战略-only trait) and no module: plain heals', () => {
|
||||
for (const f of [[5, true, PHYX], [6, true, PHYX], [5, true, PHYY], [6, true, PHYY], [6, true, ISWA], [6, true, null]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const md = modOf(tier, mod);
|
||||
if (md) assert.deepEqual([md.traitOverride.bb.heal_scale, md.traitOverride.bb.hp_ratio ?? null], [1.15, mod === PHYY ? null : 0.5], label(f));
|
||||
// 角峰 on a 地面 tile (10, 4), 德克萨斯 on high ground (11, 2)
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, mon3tr: null, others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 10, col: 4 }, { uid: 4, chessId: 'chess_char_1_08_a', row: 11, col: 2 }] });
|
||||
const yak = h.unit(3), tex = h.unit(4);
|
||||
assert.deepEqual([yak.ground, tex.ground], [true, false]);
|
||||
const healOf = (x, r) => {
|
||||
for (const o of [yak, tex]) o.hp = o.s.maxHp;
|
||||
x.hp = x.s.maxHp * r;
|
||||
const n = heals(h, u).length;
|
||||
assert.ok(h.runUntil(() => heals(h, u).length > n, 4), label(f));
|
||||
return heals(h, u)[n];
|
||||
};
|
||||
const a = healOf(yak, 0.45), b = healOf(yak, 0.8), c = healOf(tex, 0.45);
|
||||
const atk = u.s.atk;
|
||||
const want = (t, r) => atk * ((mod === PHYX && r <= 0.5) || (mod === PHYY && t.ground) ? 1.15 : 1);
|
||||
approx(a.amount, want(yak, 0.45), `${label(f)}: 地面, 45 %`);
|
||||
approx(b.amount, want(yak, 0.8), `${label(f)}: 地面, 80 %`);
|
||||
approx(c.amount, want(tex, 0.45), `${label(f)}: high ground, 45 %`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,340 @@
|
||||
// test/content/op_mcnist.test.js — the 自选 operator kit of 机械师 (char_4230_mcnist, 6★ 哨戒铁卫; kit
|
||||
// server/sim/content/kits/ops/op-mcnist.js) and her summon 结构性原理 (token_10069_mcnist_mcgraf), fielded the production way
|
||||
// (a DIY slot + its `diy` pick; the summon as a token piece of hers) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank
|
||||
// 4, no module) and elite (E2 Lv60, rank 7) with no module or SO-A 机械师特勤证章 at stage 1 (tier 5) / 3 (tier 6). Every
|
||||
// number is read back from data/backups.json (the form of that slot status, the token's variant); the fidelity checklist
|
||||
// of kits/README.md item by item.
|
||||
// Run: node --test test/content/op_mcnist.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 MCN = 'char_4230_mcnist';
|
||||
const GRAF = 'token_10069_mcnist_mcgraf';
|
||||
const FORMS = BACKUPS.units[MCN].forms;
|
||||
const TOKEN = BACKUPS.tokens[GRAF];
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const SOA = 'uniequip_002_mcnist';
|
||||
const S1 = 'skchr_mcnist_1', S2 = 'skchr_mcnist_2', S3 = 'skchr_mcnist_3';
|
||||
const statusKey = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusKey(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const variantOf = (tier, elite) => TOKEN.variants[`${MCN}@${statusKey(tier, elite)}`];
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = {
|
||||
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
|
||||
enemy_brute: dummy('enemy_brute', { atk: 2000, range: 3, bat: 1 }), enemy_poke: dummy('enemy_poke', { atk: 1, range: 3, bat: 0.5 }),
|
||||
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, SOA].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
const K = (r, c) => r * 21 + c;
|
||||
const sortN = (a) => [...a].sort((x, y) => x - y);
|
||||
|
||||
/** A battle with 机械师 as uid 1 at (10, 3) facing RIGHT (`graf`: her 结构性原理 as uid 2 at graf.row / col), plus `others`. */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, graf = { row: 10, col: 4 }, others = [], seed = 5, dp = 99 } = {}) {
|
||||
const units = [{ uid: 1, diy: { slot: SLOT[tier], charId: MCN, skillIndex: skill, uniEquipId: mod }, elite, row: 10, col: 3 }];
|
||||
if (graf) units.push({ uid: 2, kind: 'token', tokenId: GRAF, ownerUid: 1, row: graf.row, col: graf.col, dir: graf.dir });
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpInit: dp, dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'ammoUsed'], captureNoisy: true,
|
||||
units: [...units, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1), t: graf ? h.unit(2) : null };
|
||||
}
|
||||
const barrier = (x, key) => x.findBuff(key);
|
||||
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]));
|
||||
}
|
||||
|
||||
test('机械师 in every 自选 form: her operator kit (all three skills authored), stats + SO-A attributes, 2-2, blocks 3, ranged physical, hits air, 罗德岛, no 特质; her 结构性原理: the form\'s token stats, 禁疗, blocks 3, melee ground-only physical', () => {
|
||||
assert.equal(OPERATOR_KITS[MCN], KITS[MCN]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u, t } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null;
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [MCN, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp, form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [3, 'ranged', 'phys', true, 1.2], `${label(f)}: 哨戒铁卫`);
|
||||
assert.deepEqual(sortN(u.rangeKeys), sortN([K(10, 3), K(10, 4), K(10, 5)]), `${label(f)}: 2-2`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds, u.def.tokens], [BACKUPS.diy.operators[MCN].bonds, [], [GRAF]], `${label(f)}: bonds / 特质 / summon`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
const ts = variantOf(tier, elite).stats;
|
||||
assert.deepEqual([t.base.maxHp, t.base.atk, t.base.def, t.s.blockCnt, t.base.bat, t.base.cost, t.base.respawnTime], [ts.maxHp, ts.atk, ts.def, 3, 1.5, 10, 40], `${label(f)}: 结构性原理 stats`);
|
||||
assert.deepEqual([t.s.flags.noHeal, t.profile.attack, t.profile.dmgType, t.profile.canHitFly, t.profile.noAttack], [true, 'melee', 'phys', false, false], `${label(f)}: 禁疗, melee ground-only`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
// E2 Lv1 2614 / 425 / 514, E2 Lv60 3288 / 483 / 641; SO-A +60 / +40 → +100 / +80 ATK / DEF; the token 2800 / 492 / 451 → 3264 / 564 / 517
|
||||
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.def], [2614, 425, 3288, 641]);
|
||||
assert.deepEqual([modOf(5, SOA).attr, modOf(6, SOA).attr], [{ atk: 60, def: 40 }, { atk: 100, def: 80 }]);
|
||||
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(6, true).stats.maxHp], [2800, 3264]);
|
||||
});
|
||||
|
||||
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(MCN));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(MCN), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: MCN, skillIndex: 2, uniEquipId: null } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[5]]: { charId: MCN, skillIndex: 2, uniEquipId: null } } });
|
||||
});
|
||||
|
||||
test('T1 结构性原理: it lasts 30 s from each deployment (SO-A stage 3 too — its 无限 is 集成战略-only), comes back on its tile 40 s after it left paying 10 DP — only while she stands; it falls with her and her redeployment readies it at once', () => {
|
||||
for (const f of [[5, false, null], [6, true, SOA], [5, true, SOA]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0 });
|
||||
assert.equal(variantOf(tier, elite).talents.find((x) => x.bb['skill@duration'] != null).bb['skill@duration'], 30);
|
||||
h.run(29.4);
|
||||
assert.ok(t.alive, `${label(f)}: on the field before 30 s`);
|
||||
h.run(0.7);
|
||||
assert.ok(!t.alive && !t.removed, `${label(f)}: gone at 30 s (the piece stays)`);
|
||||
const dp0 = h.b.players[0].dp;
|
||||
h.run(39.4);
|
||||
assert.ok(!t.alive, `${label(f)}: not before 40 s`);
|
||||
h.run(0.7);
|
||||
assert.ok(t.alive, `${label(f)}: back after 40 s`);
|
||||
assert.deepEqual([t.tileR, t.tileC], [10, 4], label(f));
|
||||
approx(h.b.players[0].dp, dp0 - 10, `${label(f)}: 10 DP`);
|
||||
h.b.kill(u, null);
|
||||
h.step();
|
||||
assert.ok(!t.alive && !t.removed, `${label(f)}: falls with her`);
|
||||
h.run(45);
|
||||
assert.ok(!t.alive, `${label(f)}: none while she is down`);
|
||||
assert.ok(h.runUntil(() => u.alive, 60), `${label(f)}: she redeploys`);
|
||||
assert.ok(h.runUntil(() => t.alive, 0.2), `${label(f)}: with it at once (charge_token[born])`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 生命方程: she and her 结构性原理 get a 屏障 of 30 % max HP at each deployment; it takes physical and arts damage, not true; 超额防护: the hit that breaks it is cancelled whole, the next one hurts', () => {
|
||||
for (const f of [[5, false, null], [6, true, SOA]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 0 });
|
||||
const hr = formOf(tier, elite).talents.find((x) => x.index === 1).bb.hp_ratio;
|
||||
assert.equal(hr, 0.3);
|
||||
for (const x of [u, t]) {
|
||||
const b = barrier(x, 'mcnist:t2:barrier');
|
||||
approx(b.shield, x.s.maxHp * hr, `${label(f)}: ${x.kind} 屏障`);
|
||||
assert.deepEqual(b.shieldType, ['phys', 'arts'], label(f));
|
||||
}
|
||||
// true damage passes it; a physical hit bigger than it is cancelled whole; the next physical hit hurts
|
||||
const hp0 = u.hp, sh0 = barrier(u, 'mcnist:t2:barrier').shield;
|
||||
h.b.dealDamage(null, u, { amount: 100, type: 'true' });
|
||||
approx(u.hp, hp0 - 100, `${label(f)}: true damage reaches HP`);
|
||||
approx(barrier(u, 'mcnist:t2:barrier').shield, sh0, `${label(f)}: the 屏障 untouched`);
|
||||
h.b.dealDamage(null, u, { amount: 300, type: 'arts' });
|
||||
approx(barrier(u, 'mcnist:t2:barrier').shield, sh0 - 300, `${label(f)}: arts absorbed`);
|
||||
const hp1 = u.hp;
|
||||
const got = h.b.dealDamage(null, u, { amount: sh0 * 3, type: 'phys', canDodge: false });
|
||||
assert.equal(got, 0, `${label(f)}: the breaking hit is blocked whole`);
|
||||
assert.deepEqual([u.hp, barrier(u, 'mcnist:t2:barrier')], [hp1, null], `${label(f)}: HP untouched, 屏障 gone`);
|
||||
const got2 = h.b.dealDamage(null, u, { amount: 500, type: 'phys', canDodge: false });
|
||||
approx(got2, Math.max(500 - u.s.def, 25), `${label(f)}: the next hit hurts`);
|
||||
// redeployed: a new one
|
||||
h.b.kill(u, null);
|
||||
assert.ok(h.runUntil(() => u.alive, 80), label(f));
|
||||
approx(barrier(u, 'mcnist:t2:barrier').shield, u.s.maxHp * hr, `${label(f)}: a new 屏障`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 聚类分析 (AUTO, attack SP, data DEFAULT): 11 / 9 SP, 3 bullets, interval 1.2 + 1.3 s, each attack strikes 5 times every 0.2 s at 95 % / 110 % ATK physical every enemy within 1.1 of its landing point (air units too)', () => {
|
||||
for (const f of [[5, false, null], [6, true, SOA]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.skillType, sk.spType, sk.spCost, sk.bb['attack@trigger_time'], sk.bb['attack@times'], sk.bb['attack@interval'], sk.bb['attack@projectile_range'], sk.bb['attack@atk_scale']],
|
||||
['AUTO', 'INCREASE_WHEN_ATTACK', elite ? 9 : 11, 3, 5, 0.2, 1.1, elite ? 1.1 : 0.95], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, graf: null });
|
||||
assert.deepEqual([u.skill.rule, u.skill.kind, u.skill.spType], ['DEFAULT', 'ammo', 'attack'], label(f));
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 5] }), near = h.spawn('enemy_dummy', { pos: [11, 5] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 5] }), far = h.spawn('enemy_dummy', { pos: [12, 5] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `${label(f)}: cast on her next attack`);
|
||||
approx(u.s.interval, 2.5, `${label(f)}: 1.2 + 1.3 s`);
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
assert.ok(h.runUntil(() => h.hooksOf('damaged').slice(n0).some((c) => c.source === u && c.dmg?.isSkill), 3), label(f));
|
||||
h.run(1.2);
|
||||
const hits = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.dmg?.isSkill);
|
||||
const by = (e) => hits.filter((c) => c.target === e);
|
||||
assert.deepEqual([by(main).length, by(near).length, by(fly).length, by(far).length], [5, 5, 5, 0], `${label(f)}: 5 strikes on each within 1.1`);
|
||||
const atk = u.s.atk;
|
||||
for (const c of hits) approx(c.amount, atk * sk.bb['attack@atk_scale'], `${label(f)}: ${sk.bb['attack@atk_scale'] * 100} % ATK`);
|
||||
const times = by(main).map((c) => c.t);
|
||||
for (let i = 1; i < times.length; i++) approx(times[i] - times[i - 1], 0.2, `${label(f)}: 0.2 s apart`, 0.1);
|
||||
h.runUntil(() => !u.skill.active, 20);
|
||||
assert.equal(h.hooksOf('ammoUsed').filter((c) => c.unit === u).length, 3, `${label(f)}: 3 bullets`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 协防术式 (MANUAL, data DEFAULT — rawRule TAKE_DAMAGE): ATK +90 % / +120 %; she and her 结构性原理 get a 10 % 屏障; a break: 200 % ATK arts and 战栗 2 s around the holder (1.5), a bullet spent and a new 屏障; attacks spend none; the last bullet ends the skill and leaves its 屏障', () => {
|
||||
for (const f of [[5, false, null], [6, true, SOA]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.trigger.rule, sk.trigger.rawRule, sk.bb.atk, sk.bb.hp_ratio, sk.bb.trigger_time, sk.bb.range_radius, sk.bb.atk_scale, sk.bb.not_combat],
|
||||
['DEFAULT', 'TAKE_DAMAGE', elite ? 1.2 : 0.9, 0.1, elite ? 7 : 6, 1.5, 2, 2], label(f));
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 1 });
|
||||
// her T2 屏障 first spent, so the S2 one is what breaks
|
||||
h.b.removeBuff(u, 'mcnist:t2:barrier');
|
||||
const e0 = h.spawn('enemy_dummy', { pos: [10, 5] }), e1 = h.spawn('enemy_dummy', { pos: [11, 4] }), e2 = h.spawn('enemy_fly', { pos: [11, 3] }), out = h.spawn('enemy_dummy', { pos: [12, 5] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `${label(f)}: cast as she is about to attack`);
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
|
||||
for (const x of [u, t]) approx(barrier(x, 'mcnist:s2:barrier').shield, x.s.maxHp * 0.1, `${label(f)}: ${x.kind} 10 % 屏障`);
|
||||
h.run(6);
|
||||
assert.equal(u.skill.ammoLeft, sk.bb.trigger_time, `${label(f)}: attacks spend no bullet`);
|
||||
// break hers
|
||||
const hp0 = u.hp;
|
||||
const got = h.b.dealDamage(null, u, { amount: u.s.maxHp, type: 'phys', canDodge: false });
|
||||
assert.deepEqual([got, u.hp], [0, hp0], `${label(f)}: the breaking hit blocked whole (超额防护)`);
|
||||
const burst = tagged(h, u, 'mcnist:shieldBurst');
|
||||
assert.deepEqual(sortN(burst.map((c) => c.target.id)), sortN([e1.id, e2.id]), `${label(f)}: within 1.5 of her, the flyer too`);
|
||||
for (const c of burst) { approx(c.amount, u.s.atk * 2, `${label(f)}: 200 % ATK`); assert.equal(c.type, 'arts'); }
|
||||
const tr = h.hooksOf('statusApplied').filter((c) => c.status === 'tremble' && c.source === u);
|
||||
assert.deepEqual(sortN(tr.map((c) => c.target.id)), sortN([e1.id, e2.id]), `${label(f)}: 战栗`);
|
||||
for (const c of tr) approx(c.duration, 2, `${label(f)}: 2 s`);
|
||||
assert.ok(!out.findBuff('tremble') && !e0.findBuff('tremble'), `${label(f)}: 2 tiles away: none`);
|
||||
assert.equal(u.skill.ammoLeft, sk.bb.trigger_time - 1, `${label(f)}: one bullet`);
|
||||
approx(barrier(u, 'mcnist:s2:barrier').shield, u.s.maxHp * 0.1, `${label(f)}: a new 屏障`);
|
||||
// spend the rest: the skill ends, the last 屏障 stays without its break effect
|
||||
for (let i = 0; i < 20 && u.skill.active; i++) h.b.dealDamage(null, i % 2 ? t : u, { amount: 1e5, type: 'phys', canDodge: false });
|
||||
assert.ok(!u.skill.active, `${label(f)}: ended with its bullets`);
|
||||
assert.equal(h.hooksOf('ammoUsed').filter((c) => c.unit === u).length, sk.bb.trigger_time, label(f));
|
||||
const last = barrier(u, 'mcnist:s2:barrier') ?? barrier(t, 'mcnist:s2:barrier');
|
||||
assert.ok(last && last.shield > 0, `${label(f)}: the last 屏障 stays`);
|
||||
const n = tagged(h, u, 'mcnist:shieldBurst').length;
|
||||
h.b.dealDamage(null, barrier(u, 'mcnist:s2:barrier') ? u : t, { amount: 1e5, type: 'phys', canDodge: false });
|
||||
assert.equal(tagged(h, u, 'mcnist:shieldBurst').length, n, `${label(f)}: no burst after the skill`);
|
||||
done(h);
|
||||
}
|
||||
// a 结构性原理 deployed during S2 gets one at once
|
||||
const { h, u, t } = field({ skill: 1 });
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.b.retreat(t, { reason: 'expired', permanent: true });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
t.mem.readyAt = h.b.time;
|
||||
assert.ok(h.runUntil(() => t.alive, 1));
|
||||
assert.ok(barrier(t, 'mcnist:s2:barrier'), 'a deployment during S2');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 工程学十字星 (MANUAL, data DEFAULT, 40 s): ATK +160 % / +220 %, interval 1.2 + 2.3 s; each attack lands a cross 0.8 s later: 220 % / 240 % ATK arts on every enemy of the x-6 around its target\'s tile (air units too); the enemies her 结构性原理 blocks are her targets', () => {
|
||||
for (const f of [[5, false, null], [6, true, SOA]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.trigger.rule, sk.trigger.rawRule, sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.bb['attack@atk_scale'], sk.bbStr['attack@projectile_range']],
|
||||
['DEFAULT', 'TAKE_DAMAGE', 40, elite ? 2.2 : 1.6, 2.3, elite ? 2.4 : 2.2, 'x-6'], label(f));
|
||||
// the 结构性原理 on (12, 3) facing LEFT: its charge stays in place (high ground at (12, 2)), far from the targets
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, graf: { row: 12, col: 3, dir: 'LEFT' } });
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 5] }), up2 = h.spawn('enemy_fly', { pos: [12, 5] });
|
||||
const side2 = h.spawn('enemy_dummy', { pos: [10, 7] }), diag = h.spawn('enemy_dummy', { pos: [11, 6] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `${label(f)}: cast`);
|
||||
approx(u.skill.timeLeft, 40, label(f), 0.01);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
|
||||
approx(u.s.interval, 3.5, `${label(f)}: 1.2 + 2.3 s`);
|
||||
const atk = u.s.atk;
|
||||
assert.ok(h.runUntil(() => tagged(h, u, 'mcnist:cross').length > 0, 5), `${label(f)}: a cross lands`);
|
||||
const first = tagged(h, u, 'mcnist:cross');
|
||||
const t0 = first[0].t;
|
||||
const wave = first.filter((c) => Math.abs(c.t - t0) < 1e-6);
|
||||
assert.deepEqual(sortN(wave.map((c) => c.target.id)), sortN([main.id, up2.id, side2.id]), `${label(f)}: the x-6 around (10, 5)`);
|
||||
assert.ok(!wave.some((c) => c.target === diag), `${label(f)}: not the diagonal (11, 6)`);
|
||||
for (const c of wave) { approx(c.amount, atk * sk.bb['attack@atk_scale'], `${label(f)}: ${sk.bb['attack@atk_scale'] * 100} %`); assert.equal(c.type, 'arts'); }
|
||||
// 0.8 s after the throw (the attack that threw it made no damage of its own)
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && !c.dmg?.tags?.includes('mcnist:cross') && c.target.side === 'enemy' && c.dmg?.isAttack && c.t < t0).length, 0, label(f));
|
||||
done(h);
|
||||
}
|
||||
// her 结构性原理 on (9, 7), out of her range, blocks a walker: during S3 she attacks it (its charge lands in place)
|
||||
const { h, u, t } = field({ skill: 2, graf: { row: 9, col: 7, dir: 'RIGHT' } });
|
||||
const w = h.spawn('enemy_walk', { routeIndex: 0 });
|
||||
assert.ok(h.runUntil(() => t.blocking.includes(w), 30), 'the walker reaches it');
|
||||
assert.ok(!u.rangeKeySet.has(Math.round(w.y) * 21 + Math.round(w.x)), 'out of her range');
|
||||
h.run(2);
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u && c.target === w).length, 0, 'not her target without S3');
|
||||
u.skill.gainSp(999);
|
||||
u.skill.activate('test');
|
||||
assert.ok(h.runUntil(() => t.alive && t.blocking.includes(w), 1), 'its charge stops at once (the walker next to it) and it blocks again');
|
||||
assert.ok(h.runUntil(() => tagged(h, u, 'mcnist:cross').some((c) => c.target === w), 6), 'a cross on the blocked walker');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 charge: the 结构性原理 runs along its facing to the farthest free low tile before an enemy (0.2 s + the distance at 20 tiles / s), deals 300 % of her ATK physical within 1.5 of the stop tile and is deployed anew there (full HP, 生命方程 again); then a weaken aura of 1.5: 50 % − 2.5 % per second', () => {
|
||||
for (const f of [[5, false, null], [6, true, SOA]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const w = variantOf(tier, elite).talents.find((x) => x.bb.atk_interval != null).bb;
|
||||
assert.deepEqual([sk.bb.atk_scale, sk.bb.projectile_range, w.atk, w.atk_interval, w.interval, w.range_radius], [3, 1.5, -0.5, 0.025, 1, 1.5], label(f));
|
||||
const { h, u, t } = field({ tier, elite, mod, skill: 2, graf: { row: 11, col: 3, dir: 'RIGHT' } });
|
||||
const wall = h.spawn('enemy_dummy', { pos: [11, 8] }), hit2 = h.spawn('enemy_fly', { pos: [12, 7] }), front = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
t.hp = 100;
|
||||
u.skill.gainSp(999);
|
||||
const atk0 = u.s.atk;
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `${label(f)}: cast (her own target in range)`);
|
||||
const cast = h.b.time;
|
||||
const atk = u.s.atk;
|
||||
assert.ok(atk > atk0, label(f));
|
||||
assert.ok(!t.alive, `${label(f)}: off the field while it charges`);
|
||||
// from (11, 3) to (11, 7): 4 tiles ⇒ 0.2 + 4 / 20 s
|
||||
assert.ok(h.runUntil(() => t.alive, 1), `${label(f)}: lands`);
|
||||
approx(h.b.time - cast, 0.2 + 4 / 20, `${label(f)}: flight time`, 0.1);
|
||||
assert.deepEqual([t.tileR, t.tileC], [11, 7], `${label(f)}: the farthest tile before the enemy`);
|
||||
approx(t.hp, t.s.maxHp, `${label(f)}: a new deployment (full HP)`);
|
||||
approx(barrier(t, 'mcnist:t2:barrier').shield, t.s.maxHp * 0.3, `${label(f)}: 生命方程 again`);
|
||||
const ch = tagged(h, u, 'mcnist:charge');
|
||||
assert.deepEqual(sortN(ch.map((c) => c.target.id)), sortN([wall.id, hit2.id]), `${label(f)}: within 1.5 of (11, 7), the flyer too`);
|
||||
for (const c of ch) { approx(c.amount, atk * 3, `${label(f)}: 300 % of her ATK`); assert.equal(c.type, 'phys'); }
|
||||
h.run(0.3);
|
||||
const wk = (e) => h.hooksOf('statusApplied').filter((c) => c.status === 'weaken' && c.target === e);
|
||||
approx(wk(wall).at(-1).value, 0.5, `${label(f)}: 50 % at first`);
|
||||
assert.equal(wk(front).length, 0, `${label(f)}: out of the aura`);
|
||||
h.run(4);
|
||||
approx(wk(wall).at(-1).value, 0.5 - 0.025 * 4, `${label(f)}: −2.5 % per second`, 1e-6);
|
||||
h.run(17);
|
||||
const n = wk(wall).length;
|
||||
h.run(1);
|
||||
assert.equal(wk(wall).length, n, `${label(f)}: gone at 0 (20 s)`);
|
||||
done(h);
|
||||
}
|
||||
// no 结构性原理 at the cast: its next deployment during the skill charges
|
||||
const { h, u, t } = field({ skill: 2, graf: { row: 11, col: 3, dir: 'RIGHT' } });
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.b.retreat(t, { reason: 'expired', permanent: true });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
t.mem.readyAt = h.b.time;
|
||||
assert.ok(h.runUntil(() => t.alive && t.tileC !== 3, 2), 'charged after its deployment');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('SO-A 机械师特勤证章: only its attributes act (stages 1 and 3) — no 集成战略 ATK ×1.5 on blocked enemies, the 结构性原理 keeps its 30 s', () => {
|
||||
for (const [tier, mod] of [[5, SOA], [6, SOA], [6, null]]) {
|
||||
const { h, u, t } = field({ tier, elite: true, mod, skill: 0, graf: { row: 10, col: 4 } });
|
||||
const m = modOf(tier, mod);
|
||||
if (m) assert.match(m.traitOverride.moduleDesc, /沉沦者的黑流树海/);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
h.run(3);
|
||||
const hits = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.target === e && c.dmg?.isAttack && !c.dmg?.isSkill);
|
||||
assert.ok(hits.length > 0, 'attacks');
|
||||
for (const c of hits) approx(c.amount, u.s.atk, `T${tier} ${mod}: plain ATK`);
|
||||
h.run(28);
|
||||
assert.ok(!t.alive, `T${tier} ${mod}: the 结构性原理 left at 30 s`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
+357
-117
@@ -1974,8 +1974,8 @@
|
||||
"snaps": "8eac4d32 fa5c2376 bc1e9184 84fb1db6 b4b3b807 cc6f6399 c665e4ff 261094cd 5d5c61fd 1bf27396 b526ffa6 e433a3a2 7d8bd521 d7eb687d 4d18ecc9 f640c5fd 0912a7d6 cc22df71 1a833e56 d877d28b 8a887ef8 8014c7dd"
|
||||
},
|
||||
"diy-049": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
|
||||
"fast": true,
|
||||
"about": "5_diy1_a=jesca2/S1 5_diy2_a=jesca2/S2 5_diy1_a=jesca2/S3 5_diy2_b=jesca2/S1 5_diy1_b=jesca2/S2 5_diy2_b=jesca2/S3 5_diy1_b=jesca2/S1/002_jesca2 5_diy2_b=jesca2/S2/002_jesca2 5_diy1_b=jesca2/S3/002_jesca2 5_diy2_b=jesca2/S1/003_jesca2 5_diy1_b=jesca2/S2/003_jesca2 5_diy2_b=jesca2/S3/003_jesca2",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m05",
|
||||
"modeId": "mode_multi_normal",
|
||||
@@ -1985,122 +1985,115 @@
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 2,
|
||||
"total": 60,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":2,"total":60,"perfect":false,"leaks":58,"leaksUncounted":0,"coins":0,"dmg":267738,"bossDmg":0,"heal":59968,"deaths":6,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,76,0,304,6384,0,22,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,75,0,652,8724,0,41,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,6,0,21282,21790,0,137,3],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",3334,1,8,2,22607,38177,0,115,4],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1271,1,10,0,2272,0,0,99,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,11,0,2916,0,0,108,3],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,1,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,3,0,97722,0,0,178,4],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,5,0,0,0,29529,69,4],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,7,0,0,0,30439,60,4],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,9,0,56880,0,0,144,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,12,0,63104,0,0,136,3]
|
||||
],
|
||||
"enemies": {"enemy_1045_hammer_2":[12,0,11,37471,77249],"enemy_1061_zomshd":[48,2,45,37605,269489]},
|
||||
"hooks": [1,76,6900,1205,1205,3051,3051,29902,8,64,33,29,0,37113,285,23,60,56,1,8,0,0,0],
|
||||
"events": {"atk":1205,"deploy":16,"die":8,"dmg":3051,"heal":129,"leak":56,"skill":62,"spawn":72,"status":555},
|
||||
"fx": {},
|
||||
"snaps": "c6bdc18a 6fb12fe1 c3c94d6f f42e9bba 6ea4c93a ff3506ac f551dbb2 248e7a29 e877982d 0b8ac60e d21ffc26 89eda7b5 7ef13ac0 2d2fce6f 32484e38 3407d0b5 1b59f452 55141007 fc87be97 6239dcab e11e96f9 250fe5b6 a75b766c"
|
||||
},
|
||||
"diy-050": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m06",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 12,
|
||||
"seed": 574294847,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 214.8,
|
||||
"ticks": 6444,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 60,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 115,
|
||||
"rngDraws": 13,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":60,"perfect":false,"leaks":60,"leaksUncounted":0,"coins":0,"dmg":157435,"bossDmg":0,"heal":12039,"deaths":23,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":0,"total":60,"perfect":false,"leaks":60,"leaksUncounted":0,"coins":0,"dmg":60775,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,82,0,28,5208,0,2,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",3371,1,6,0,0,0,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1529,1,9,0,20856,0,0,0,7],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,72,0,174,5712,0,6,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,67,0,13430,5276,0,29,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,78,0,61051,12366,0,48,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,85,0,698,3816,0,13,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,84,0,2755,4761,0,39,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,52,0,19051,2744,0,7,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,51,0,39393,3498,0,7,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,47,0,0,2448,1260,3,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,54,0,0,9324,10779,8,1]
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,2589,1,82,0,714,5178,0,33,0],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,63,0,1651,5178,0,56,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,73,0,11244,5178,0,29,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,64,0,1599,6530,0,65,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",2866.3,1,81,0,4320,6929,0,124,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,79,0,12208,9795,0,51,1],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_jesca2",0,0,43,0,650,6890,0,24,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_jesca2",3445,1,1,0,0,0,0,0,0],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_jesca2",3445,1,3,0,0,0,0,0,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_jesca2",3445,1,5,0,0,0,0,0,1],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_jesca2",3445,1,7,0,1387,0,0,51,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_jesca2",3445,1,9,0,27000,0,0,53,2]
|
||||
],
|
||||
"enemies": {"enemy_1045_hammer_2":[12,0,12,15694,22503],"enemy_1061_zomshd":[48,0,48,39458,134932]},
|
||||
"hooks": [1,85,6444,245,245,412,396,10112,23,83,15,15,0,16629,0,28,60,60,1,26,0,0,16],
|
||||
"events": {"atk":263,"deploy":25,"die":23,"dmg":396,"fx":86,"heal":23,"leak":60,"skill":30,"spawn":72,"status":14},
|
||||
"fx": {"aoe":5,"dodge":16,"dp":7,"slash":56,"spGain":2},
|
||||
"snaps": "580b238a aa91295b 3af047b3 a833e612 f6a0b595 1eba1563 0c047aab 7838a66c 34fc5adc 627a4814 4d7c5d84 41a4b79a 721f8563 f6d5a347 76cda6c2 74e73fdb 220b1dd6 2f7b90e4 d3a2ce26 6a84c6a3 1aae6574 0e8ef572"
|
||||
"enemies": {"enemy_1045_hammer_2":[12,0,12,22695,20693],"enemy_1061_zomshd":[48,0,45,22982,118642]},
|
||||
"hooks": [1,82,6900,646,646,2610,2599,16240,15,72,8,6,61,17583,2,48,60,57,1,15,0,0,11],
|
||||
"events": {"atk":646,"deploy":22,"die":15,"dmg":2599,"fx":11,"leak":57,"skill":14,"spawn":72,"status":4},
|
||||
"fx": {"dodge":11},
|
||||
"snaps": "ba42617b c56f4cd7 32daf4ba ea7b16a6 c678eaa2 d276ac30 f459ee24 a4cb853b 5bf55077 5909a19c 1c05d106 f20147ed 99f86152 1ffdfdca ead5fe0a 10aef045 88bff8b4 917bbcf3 5be27532 e074c221 6e393a99 fa748234 81823f98"
|
||||
},
|
||||
"diy-050": {
|
||||
"about": "6_diy1_a=jesca2/S1 6_diy2_a=jesca2/S2 6_diy1_a=jesca2/S3 6_diy2_b=jesca2/S1 6_diy1_b=jesca2/S2 6_diy2_b=jesca2/S3 6_diy1_b=jesca2/S1/002_jesca2 6_diy2_b=jesca2/S2/002_jesca2 6_diy1_b=jesca2/S3/002_jesca2 6_diy2_b=jesca2/S1/003_jesca2 6_diy1_b=jesca2/S2/003_jesca2 6_diy2_b=jesca2/S3/003_jesca2",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m06",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 13,
|
||||
"seed": 574294847,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 209.53,
|
||||
"ticks": 6286,
|
||||
"reason": "cleared",
|
||||
"killed": 6,
|
||||
"total": 105,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 21,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":6,"total":105,"perfect":false,"leaks":99,"leaksUncounted":0,"coins":0,"dmg":142184,"bossDmg":0,"heal":0,"deaths":18,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,126,0,974,7767,0,7,0],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,2589,1,7,0,25079,0,0,152,4],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,2589,1,9,0,31271,0,0,37,2],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,109,0,221,9795,0,9,0],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,96,0,4856,6530,0,50,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,74,0,3593,6530,0,11,1],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_jesca2",3515,1,119,0,390,7030,0,14,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_jesca2",0,0,4,2,31909,3515,0,44,1],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_jesca2",0,0,70,1,6436,7030,0,12,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_jesca2",0,0,1,0,1351,3590,0,9,0],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_jesca2",0,0,3,3,25423,3590,0,50,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_jesca2",0,0,5,0,10681,3590,0,12,1]
|
||||
],
|
||||
"enemies": {"enemy_1165_duhond":[51,6,45,1745,109362],"enemy_1174_duholy_2":[24,0,24,20766,26233],"enemy_1175_dushdo_2":[30,0,30,36455,6588]},
|
||||
"hooks": [1,126,6286,530,530,528,514,0,24,123,12,11,50,15342,252,47,105,99,1,24,0,0,14],
|
||||
"events": {"atk":530,"deploy":21,"die":24,"dmg":514,"fx":23,"leak":99,"skill":23,"spawn":117,"status":169},
|
||||
"fx": {"dodge":14,"reveal":9},
|
||||
"snaps": "78be456d 78e48ca9 2bd9c210 8143b52a fcfaf8af 3ff43951 8697d9a4 a26d6b97 7dfd1fba 2c7577c2 5bc39b67 c5d496d2 45c4f58c aadfe7e0 1a837780 44c6cc25 b2a509a8 6c51e37f 481866ab f58770e1 803806bd"
|
||||
},
|
||||
"diy-051": {
|
||||
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
|
||||
"about": "5_diy1_a=mcnist/S1 5_diy2_a=mcnist/S2 5_diy1_a=mcnist/S3 5_diy2_b=mcnist/S1 5_diy1_b=mcnist/S2 5_diy2_b=mcnist/S3 5_diy1_b=mcnist/S1/002_mcnist 5_diy2_b=mcnist/S2/002_mcnist 5_diy1_b=mcnist/S3/002_mcnist 6_diy1_a=mcnist/S1 6_diy2_a=mcnist/S2 6_diy1_a=mcnist/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m07",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"round": 12,
|
||||
"seed": 3864737019,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 204.8,
|
||||
"ticks": 6144,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 201.47,
|
||||
"ticks": 6044,
|
||||
"reason": "cleared",
|
||||
"killed": 197,
|
||||
"total": 290,
|
||||
"killed": 83,
|
||||
"total": 99,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 282,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":197,"total":290,"perfect":false,"leaks":93,"leaksUncounted":0,"coins":0,"dmg":444885,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":83,"total":99,"perfect":false,"leaks":16,"leaksUncounted":0,"coins":0,"dmg":1015722,"bossDmg":0,"heal":0,"deaths":14,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,4,7,21881,2604,0,65,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,7,11,35637,3431,0,87,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,1560.61,1,279,11,28493,2169,0,91,2],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",1890.36,1,277,51,98505,2858,0,110,3],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,241,13,39114,3058,0,54,3],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,163,43,73993,3808,0,55,4],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,285,5,26351,8078,0,87,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,3,7,19,0,0,16,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,2638,1,5,10,60,0,0,60,3],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",2339.65,1,8,13,64107,1191,0,82,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,6,10,18397,0,0,78,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,9,16,38328,0,0,93,2],
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,2614,1,120,1,21835,2614,0,22,1],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,6,9,61469,2614,0,79,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,2614,1,112,8,116469,2614,0,50,2],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",3288,1,78,9,65023,3288,0,68,5],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",3288,1,95,5,72011,3288,0,101,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,81,16,217675,6576,0,48,2],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_mcnist",0,0,70,13,187327,6576,0,83,6],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_mcnist",3288,1,3,11,90830,0,0,79,1],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_mcnist",3288,1,122,6,50371,3288,0,24,1],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,82,0,20215,5228,0,44,2],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,9,3,46785,2614,0,74,1],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,1100.17,1,109,2,65712,4128,0,67,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1195_sfyin": [39,30,9,4143,113683],
|
||||
"enemy_1196_msfyin": [60,32,28,0,65],
|
||||
"enemy_1197_sfshu": [48,38,10,10130,184299],
|
||||
"enemy_1198_msfshu": [114,78,36,0,243],
|
||||
"enemy_1199_sfjin": [18,11,7,12925,146349],
|
||||
"enemy_1200_msfjin": [11,8,3,0,246]
|
||||
},
|
||||
"hooks": [1,314,6144,1470,1470,1969,2082,0,212,305,25,30,0,33234,6218,91,290,93,1,212,0,0,11],
|
||||
"events": {"atk":1512,"deploy":22,"die":212,"dmg":2076,"fx":273,"leak":93,"skill":48,"spawn":304,"status":488},
|
||||
"fx": {"aoe":56,"displace":1,"dodge":22,"dp":63,"slash":52,"summon":79},
|
||||
"snaps": "cf051c06 587dcba3 94ed6adb bba4fd81 815661ad cbf17744 a8d0c19b 32b88916 1e8bde83 51a1d669 b023b185 df725cf4 bf4681aa 2c372e04 1a001766 3d81d3d6 7f8effe5 11826d80 b4fb05d0 0eed6d6c 65f6503d"
|
||||
"enemies": {"enemy_1010_demon":[42,38,4,14216,449007],"enemy_1320_wdrrl_2":[12,5,7,26818,212204],"enemy_1422_lrsldr_2":[45,40,5,1794,354511]},
|
||||
"hooks": [1,122,6044,1411,1411,2480,2480,0,97,113,26,22,73,14068,101,105,99,16,1,97,0,0,0],
|
||||
"events": {"atk":1411,"deploy":21,"die":97,"dmg":2135,"fx":152,"leak":16,"skill":48,"spawn":113,"status":308},
|
||||
"fx": {"aoe":86,"explode":8,"shield":58},
|
||||
"snaps": "bafd7890 0f8e39c5 de2bf506 5cee072b 4131c0d3 130e376f 082e40af 1afd8471 ea4c89d1 957cfcf6 4542daa0 986dd0c1 a2fbd981 c88fa318 d7590fbf c396e408 93cb626c 5a3e8085 0e32ffe5 29ea4aca fb80caed"
|
||||
},
|
||||
"diy-052": {
|
||||
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
|
||||
"about": "6_diy1_b=mcnist/S1 6_diy2_b=mcnist/S2 6_diy1_b=mcnist/S3 6_diy2_b=mcnist/S1/002_mcnist 6_diy1_b=mcnist/S2/002_mcnist 6_diy2_b=mcnist/S3/002_mcnist 5_diy1_a=kalts/S1 5_diy2_a=kalts/S2 5_diy1_a=kalts/S3 5_diy2_b=kalts/S1 5_diy1_b=kalts/S2 5_diy2_b=kalts/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m01",
|
||||
@@ -2108,35 +2101,282 @@
|
||||
"round": 13,
|
||||
"seed": 1210502478,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 218.57,
|
||||
"ticks": 6557,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 35,
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 81,
|
||||
"rngDraws": 3,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":76446,"bossDmg":0,"heal":2762,"deaths":33,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":35,"total":123,"perfect":false,"leaks":88,"leaksUncounted":0,"coins":0,"dmg":1203339,"bossDmg":0,"heal":46130,"deaths":16,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,6,0,2101,1865,0,8,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,9,0,4660,2494,0,8,0],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,157,0,13058,11540,0,21,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,156,0,24836,14840,0,17,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,151,0,3559,2544,0,18,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,3,0,11692,1643,0,23,0],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,4,0,9700,1372,0,5,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,11,0,1022,1819,0,1,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,86,0,0,4120,2092,4,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,74,0,0,4018,670,2,0],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,1,0,1487,1160,0,3,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,5,0,4330,1450,0,7,1]
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"none",3288,1,120,4,209779,8289,0,78,6],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"none",2273.39,1,88,8,235218,4695,0,116,2],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"none",2487.16,1,141,4,103454,8161,0,33,2],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_mcnist",3288,1,145,4,99030,6066,0,34,3],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_mcnist",1475.69,1,77,4,217574,8330,0,122,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_mcnist",0,0,8,11,338283,9833,0,63,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,1469,1,2,0,0,4598,7788,43,4],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,3,0,0,1861,16009,46,0],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,4,0,0,9599,13155,45,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,130,0,0,6378,759,2,0],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",1578,1,146,0,0,6892,4193,10,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,89,0,0,12056,4228,15,0]
|
||||
],
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,0,57,29550,4113],"enemy_1299_ymkilr_2":[27,0,27,14576,26756],"enemy_1422_lrsldr_2":[39,0,39,4739,45577]},
|
||||
"hooks": [1,157,6557,277,277,283,291,7,33,156,4,24,0,6272,192,29,123,123,1,33,0,0,0],
|
||||
"events": {"atk":278,"deploy":34,"die":33,"dmg":269,"fx":19,"heal":7,"leak":123,"skill":8,"spawn":135,"status":92},
|
||||
"fx": {"aoe":2,"displace":15,"spGain":2},
|
||||
"snaps": "21be44ba 23e43266 29788d55 2f1037e9 363ad836 051606c5 26acdfeb 8dc54607 7a7c6a86 7ebe59b3 d1661fdb 203d434c 2e82423b f7e9ffc1 7a1afa0e 8998df31 0ca38cb6 f3ac5677 87bcc7cf 0f5f3879 0e082cac 62241cef"
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,24,32,48946,474937],"enemy_1299_ymkilr_2":[27,0,27,32191,120187],"enemy_1422_lrsldr_2":[39,11,27,5621,608215]},
|
||||
"hooks": [1,146,6900,1681,1681,2454,2452,161,51,137,21,18,47,12458,189,63,123,86,1,51,0,0,0],
|
||||
"events": {"atk":1681,"deploy":23,"die":51,"dmg":2054,"fx":90,"heal":161,"leak":86,"skill":39,"spawn":135,"status":436},
|
||||
"fx": {"aoe":50,"block":2,"shield":38},
|
||||
"snaps": "0e9d4d53 a00748cc 605af3f8 d326a721 c4b0bb47 9b322b84 cdd1f1f3 762e9810 69ef27a1 d8725a0e 2a1640c8 a7234100 029dc53a b7164144 432d72a5 364f4796 70c22926 f25672f6 a6b2b85e c0dec1bd 4b736b32 bbe18387 53db4d81"
|
||||
},
|
||||
"diy-053": {
|
||||
"about": "5_diy1_b=kalts/S1/002_kalts 5_diy2_b=kalts/S2/002_kalts 5_diy1_b=kalts/S3/002_kalts 5_diy2_b=kalts/S1/003_kalts 5_diy1_b=kalts/S2/003_kalts 5_diy2_b=kalts/S3/003_kalts 5_diy1_b=kalts/S1/004_kalts 5_diy2_b=kalts/S2/004_kalts 5_diy1_b=kalts/S3/004_kalts 6_diy1_a=kalts/S1 6_diy2_a=kalts/S2 6_diy1_a=kalts/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m02",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"seed": 2850577873,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 0,
|
||||
"total": 114,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":114,"perfect":false,"leaks":114,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":66122,"deaths":0,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_kalts",1578,1,6,0,0,0,9962,74,4],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_kalts",1578,1,7,0,0,0,10234,74,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_kalts",1578,1,13,0,0,0,4936,37,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_kalts",1693,1,4,0,0,0,9856,44,4],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_kalts",1693,1,5,0,0,0,8288,37,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_kalts",1693,1,8,0,0,0,0,0,0],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_004_kalts",1578,1,14,0,0,0,4802,36,4],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_004_kalts",1578,1,9,0,0,14541,667,5,0],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_004_kalts",1578,1,12,0,0,14941,0,0,0],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,1469,1,10,0,0,9856,0,0,0],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,1469,1,11,0,0,9408,0,0,0],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,1469,1,3,0,0,17377,17377,75,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,0,42,46858,0],"enemy_1169_duphlx_2":[51,0,49,19264,0],"enemy_1172_dugago":[18,0,18,0,0]},
|
||||
"hooks": [1,128,6900,796,796,414,414,382,0,109,12,9,0,8820,0,5,114,109,1,0,0,0,0],
|
||||
"events": {"atk":796,"deploy":12,"dmg":414,"heal":382,"leak":109,"skill":21,"spawn":128,"status":114},
|
||||
"fx": {},
|
||||
"snaps": "e92af7ef bf43d79f adbe0394 444aa075 5d35943e 9aad9b42 03fe0969 9248bfd2 bfa47b08 7884deab 3b839490 1b069526 a6d98e95 88dbd298 1697e311 52f8b2be 700a5aa5 d017b2af 47953ad9 c54a9945 d41990e7 95af588c 243cb2c1"
|
||||
},
|
||||
"diy-054": {
|
||||
"about": "6_diy1_b=kalts/S1 6_diy2_b=kalts/S2 6_diy1_b=kalts/S3 6_diy2_b=kalts/S1/002_kalts 6_diy1_b=kalts/S2/002_kalts 6_diy2_b=kalts/S3/002_kalts 6_diy1_b=kalts/S1/003_kalts 6_diy2_b=kalts/S2/003_kalts 6_diy1_b=kalts/S3/003_kalts 6_diy2_b=kalts/S1/004_kalts 6_diy1_b=kalts/S2/004_kalts 6_diy2_b=kalts/S3/004_kalts",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m03",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"seed": 196461732,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 0,
|
||||
"total": 66,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 13,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":66,"perfect":false,"leaks":66,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":32620,"deaths":19,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"none",1578,1,91,0,0,4592,3730,19,2],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"none",1578,1,92,0,0,3972,3508,10,0],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"none",1578,1,89,0,0,6175,2765,11,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_kalts",0,0,56,0,0,18504,1311,4,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_kalts",0,0,76,0,0,4268,2696,7,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_kalts",0,0,85,0,0,4271,3091,8,0],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_003_kalts",0,0,5,0,0,1773,582,2,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_003_kalts",0,0,7,0,0,2355,2900,9,0],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_003_kalts",1773,1,10,0,0,0,4628,15,0],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_004_kalts",0,0,61,0,0,5942,1896,6,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_004_kalts",0,0,57,0,0,8859,2747,10,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_004_kalts",0,0,6,0,0,2281,2765,8,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],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[12,0,11,2947,0],"enemy_1041_lazerd_2":[24,0,24,60045,0],"enemy_1042_frostd":[30,0,30,0,0]},
|
||||
"hooks": [1,92,6900,293,293,166,160,109,19,84,5,4,0,2850,0,0,66,65,1,19,0,0,0],
|
||||
"events": {"atk":293,"deploy":23,"die":19,"dmg":160,"fx":6,"heal":109,"leak":65,"skill":9,"spawn":81,"status":52},
|
||||
"fx": {"block":6},
|
||||
"snaps": "12a65e4e b2393bab 4afaef8d 7cf86c19 eddabc86 09ba94d6 ef1914ed 6937fc3e 3bef265d e93f87c7 944d68b2 046c5938 4f56e13f 5dd4f708 e9639686 02fbe216 01335f03 37322cb1 449f21b1 f6747c86 8e1171a7 b1229590 c708ef68"
|
||||
},
|
||||
"diy-055": {
|
||||
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
|
||||
"fast": true,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m04",
|
||||
"modeId": "mode_multi_normal",
|
||||
"round": 12,
|
||||
"seed": 1597707536,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 226.43,
|
||||
"ticks": 6793,
|
||||
"reason": "cleared",
|
||||
"killed": 8,
|
||||
"total": 117,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":8,"total":117,"perfect":false,"leaks":109,"leaksUncounted":0,"coins":0,"dmg":52598,"bossDmg":0,"heal":8900,"deaths":25,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,117,0,0,6078,0,0,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,111,0,0,7823,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,144,0,0,5128,0,0,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",208.49,1,145,0,0,10902,0,0,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,132,0,18064,2542,0,48,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",868.62,1,146,5,21138,3661,0,44,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,86,1,660,2480,0,2,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,79,1,4810,2924,0,9,0],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,5,0,0,1218,5678,12,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,92,0,0,4841,3223,7,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,142,0,3950,2796,0,11,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,135,1,3976,4845,0,9,0],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai":[51,8,43,0,18454],"enemy_1005_yokai_2":[54,0,54,21039,2515],"enemy_1005_yokai_3":[12,0,12,34200,31629]},
|
||||
"hooks": [1,146,6793,492,492,425,425,19,33,142,4,4,0,11479,18,0,117,109,1,33,0,0,0],
|
||||
"events": {"atk":492,"deploy":27,"die":33,"dmg":425,"heal":19,"leak":109,"skill":8,"spawn":131,"status":28},
|
||||
"fx": {},
|
||||
"snaps": "e90d8ac5 3e7826d8 2ea79392 0d0a30f6 55810e9b a62c5a87 b1c5cfca 6959234f 84a973da 3386636e fa19929f 31ac9ec4 cc1b23bc 0ce6593e 1aa312f9 8de415cc 7670e151 f5f13c6f 12c4e5a4 75fb7a93 385d158a f1ce79bb 455d3b6d"
|
||||
},
|
||||
"diy-056": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m01",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 12,
|
||||
"seed": 3238296381,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 208.03,
|
||||
"ticks": 6241,
|
||||
"reason": "cleared",
|
||||
"killed": 3,
|
||||
"total": 75,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 196,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":3,"total":75,"perfect":false,"leaks":72,"leaksUncounted":0,"coins":0,"dmg":162615,"bossDmg":0,"heal":9891,"deaths":23,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,7,0,249,2808,0,3,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,9,1,600,3371,0,5,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,97,1,12118,4587,0,5,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,101,0,24851,5712,0,11,1],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,68,0,6186,5276,0,8,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,8,0,35092,8965,0,13,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,75,0,17393,2748,0,33,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,84,1,48820,3924,0,40,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,61,0,8325,3344,0,4,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,55,0,8980,3498,0,4,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,93,0,0,3198,8062,15,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,102,0,0,4657,1829,3,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],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1267_nhpbr_2":[36,2,34,5542,68908],"enemy_1270_nhstlk_2":[27,1,26,12814,85923],"enemy_1272_nhtank_2":[12,0,12,33732,7784]},
|
||||
"hooks": [1,102,6241,222,222,529,526,32,26,98,6,6,0,8985,0,21,75,72,1,56,0,0,3],
|
||||
"events": {"atk":261,"deploy":24,"die":26,"dmg":526,"fx":42,"heal":32,"leak":72,"skill":12,"spawn":90,"status":32},
|
||||
"fx": {"dodge":3,"dp":3,"slash":16,"spGain":2,"zone":18},
|
||||
"snaps": "4a66aa50 cd957f8d 29040588 85dbfdfb 24a06993 403a5cd0 af8c7db4 ece6f6e6 e208e92d b00a9483 cdbb6067 b86efe8c 4d65b9e6 9e9b8ccc 4dd884e0 de6f0bb5 1ee54cb6 187ba789 edc6ec98 27566532 7101f464"
|
||||
},
|
||||
"diy-057": {
|
||||
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m02",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"seed": 583790378,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 229.57,
|
||||
"ticks": 6887,
|
||||
"reason": "cleared",
|
||||
"killed": 25,
|
||||
"total": 147,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 349,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":25,"total":147,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":447188,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,144,0,5014,5208,0,37,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,1,0,7239,3431,0,80,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,143,0,12621,3730,0,59,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,139,5,129318,4748,0,83,2],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1529,1,176,2,19391,3058,0,28,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1904,1,174,8,121508,3808,0,33,4],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,165,0,5198,6924,0,54,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,171,0,7386,4344,0,52,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,2638,1,177,2,17400,2638,0,21,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,146,6,70277,7062,0,82,2],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,6,0,21302,0,0,139,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,8,2,30532,0,0,150,3]
|
||||
],
|
||||
"enemies": {"enemy_1166_dusbr_2":[39,10,29,2081,102398],"enemy_1169_duphlx_2":[57,9,48,10670,156040],"enemy_1170_dushld_2":[51,6,45,32200,188750]},
|
||||
"hooks": [1,177,6887,1087,1087,1627,1734,0,48,170,21,28,0,26130,14208,52,147,122,1,48,0,0,14],
|
||||
"events": {"atk":1145,"deploy":30,"die":48,"dmg":1705,"fx":155,"leak":122,"skill":40,"spawn":159,"status":1199},
|
||||
"fx": {"aoe":51,"displace":8,"dodge":32,"dp":16,"slash":48},
|
||||
"snaps": "3c92533d c853aee7 5b1063fd 5fc57b7e 4a05e8e6 38a71bec 3070ca8c d432fa88 49683316 30a3854a 9104d2b4 923ae15a 22aa5459 cdd051aa 9f802034 82bafa84 ccf4b89d e3e17beb 6eb62b58 fe7abec6 989ccd10 d648552b 98e9bf71"
|
||||
},
|
||||
"diy-058": {
|
||||
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m03",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 13,
|
||||
"seed": 2224511831,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 208.07,
|
||||
"ticks": 6242,
|
||||
"reason": "cleared",
|
||||
"killed": 2,
|
||||
"total": 105,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 148,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":2,"total":105,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":0,"dmg":240102,"bossDmg":0,"heal":14281,"deaths":30,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,5,0,14676,1865,0,12,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,7,1,65680,3799,0,21,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,140,0,19129,12041,0,30,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,139,0,38020,15591,0,35,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,131,0,5981,1272,0,26,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,12,0,8584,1643,0,26,0],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,78,0,5748,4303,0,7,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,4,1,75199,1819,0,10,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,6,0,0,2056,5589,7,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,9,0,0,5567,8692,11,1],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,13,0,3038,1160,0,13,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,73,0,4048,5494,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_1229_darmy":[27,1,26,5045,88057],"enemy_1229_darmy_2":[57,1,56,19387,116235],"enemy_1275_dwlock_2":[21,0,21,32176,35810]},
|
||||
"hooks": [1,140,6242,294,294,501,589,31,32,135,6,24,0,7426,388,24,105,103,1,32,0,4,3],
|
||||
"events": {"atk":328,"deploy":32,"die":32,"dmg":541,"fx":78,"heal":27,"leak":103,"skill":12,"spawn":120,"status":178},
|
||||
"fx": {"aoe":8,"beam":24,"burst":4,"displace":25,"dodge":5,"spGain":12},
|
||||
"snaps": "c9dff647 505536a6 046d3d99 29e1ce4f 95f692ed cb471e6e 3e929e64 72083a81 77cee0d9 d920f33c d26b1231 10d91924 2bc3222a 3b3d8eb1 a41709a6 a48f0d23 774d80e5 2256086c dbd65fc9 29f0c70c fa23ead3"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user