feedback5 O23: 自选 kits for 白铁, 娜斯提 (with their 支援装置) and 死芒 (with her 悲叹的仆役); ally targets for 白铁's 铁钳号·原型机
- kits/ops/op-ironmn.js — 白铁 (char_4072_ironmn): S1 / S2 / S3, 战地工程师 (device stock 3, CRA-X +1, refilled at each deployment; a device that left comes back on its tile after its redeploy time, paying its cost, one stock, while he stands; his devices leave with him), 节约经费 (prob 0.7 / CRA-X stage 3 0.9), CRA-X (stock, costs, stage 3 SP +0.2/s), CRA-Y (redeploy times, stage 3 ASPD +6 on device-buffed operators); the device kits 白铁™多功能平台 (S1 ATK +12 % ×2.5 / 3 in S1; S2 1 SP / 3.5 s, 2.5 / 2 s in S2, 0.4 % / s 流失 ×2) and 铁钳号·原型机 (an ally target: damage cancelled, 1 SP per hit, a 工匠's hit heals 1 %; its skill −2.5 % then 230 / 260 % + 110 / 120 % within 1.1 of 白铁's ATK; 团结的力量 ×0.8 physical behind it). Exports the shared 工匠 device helpers. - kits/ops/op-nasti.js — 娜斯提 (char_4212_nasti): S1 / S2 / S3, 前方施工 (stock; CRA-X stage 3 first device −3, combined ranges), 注意安全 (10 % 庇护, 15 % + 1 SP / 6 s for her and our ranged operators once a ranged enemy appeared), CRA-X; the device kits “质检专员”, “监工专员” (3 / 8 HP, its S2 pulses of SP and her barrier) and “应急承重小组” (小工程师 + the 高台 it builds and levels). - kits/ops/op-necras.js — 死芒 (char_450_necras): S1 / S2 / S3, 复燃 (summon on the kill tile or a random tile of her range, upgrade at 3), 回光黯淡, the trait (the enemies her summons block are her targets; SOC-X ×1.15), SOC-X stage 3; the 悲叹的仆役 kit and its special form (S3). - Engine (additive): Battle.setAllyTarget / allyTargetsInKeys / isAllyTarget (battle/queries.js); ai.js acquireTargets puts a registered ally target of the range after every enemy, performAttack flies ranged attacks to it. Nothing changes while none is registered: every golden family but diy unchanged. docs/SIM.md §6 documents it. - kits/index.js: the three files appended to OPERATOR_KIT_FILES. - Tests: test/content/op_ironmn.test.js, op_nasti.test.js, op_necras.test.js, test/sim/feedback5-o23-engine.test.js. - Golden: only diy moved — diy-106 … diy-109 re-chunked (the new operators' configurations come before the prototypes), diy-110 … diy-114 new.
This commit is contained in:
1 parent
996f5d20fe
commit
5ef47b0cf1
12 files changed
+2859
-100
No files matched your search
@@ -0,0 +1,457 @@
|
||||
// test/content/op_ironmn.test.js — the 自选 operator kit of 白铁 (char_4072_ironmn, 6★ 工匠; kit
|
||||
// server/sim/content/kits/ops/op-ironmn.js) and of his <支援装置> 白铁™多功能平台 (token_10027_ironmn_pile1 / pile2) and 铁钳号·原型机
|
||||
// (token_10027_ironmn_pile3), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy; the devices as the placed
|
||||
// hand pieces of his player) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7)
|
||||
// with no module, CRA-X 铁钳号·爬行者 or CRA-Y 海布里印记 at stage 1 (tier 5) / 3 (tier 6). Numbers from data/backups.json; the
|
||||
// fidelity checklist of kits/README.md (the engine's ally targets themselves: test/sim/feedback5-o23-engine.test.js).
|
||||
// Run: node --test test/content/op_ironmn.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks, diyRecordOf } from '../../shared/diy.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const IRON = 'char_4072_ironmn';
|
||||
const P1 = 'token_10027_ironmn_pile1', P2 = 'token_10027_ironmn_pile2', CRAB = 'token_10027_ironmn_pile3';
|
||||
const TOK = [P1, P2, CRAB];
|
||||
const FORMS = BACKUPS.units[IRON].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const X = 'uniequip_002_ironmn', Y = 'uniequip_003_ironmn';
|
||||
const S1 = 'skchr_ironmn_1', S2 = 'skchr_ironmn_2', S3 = 'skchr_ironmn_3';
|
||||
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
/** A device's variant for a form, with the pick's skill / module (getDiyToken). */
|
||||
const tokOf = (id, tier, elite, skill, mod) => {
|
||||
let v = BACKUPS.tokens[id].variants[`${IRON}@${statusOf(tier, elite)}`];
|
||||
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
|
||||
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
|
||||
return v;
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = {
|
||||
enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }),
|
||||
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }),
|
||||
enemy_hit: dummy('enemy_hit', { atk: 1000, bat: 1, blockCnt: 1 }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, X, Y].map((m) => [t, true, m]))];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/**
|
||||
* 白铁 as uid 1 at (row, col) facing `dir`; `pieces` = his device pieces [[r, c, dir?]] (uids 10…, the picked skill's
|
||||
* device); `others` = more units.
|
||||
*/
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, dir = 'RIGHT', pieces = [], others = [], seed = 5, dp = 99 } = {}) {
|
||||
const units = [
|
||||
{ uid: 1, diy: { slot: SLOT[tier], charId: IRON, skillIndex: skill, uniEquipId: mod }, elite, row, col, dir },
|
||||
...pieces.map(([r, c, d], i) => ({ uid: 10 + i, kind: 'token', tokenId: TOK[skill], ownerUid: 1, row: r, col: c, dir: d ?? 'RIGHT' })),
|
||||
...others,
|
||||
];
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'death', 'deploy', 'statusApplied', 'spGain', 'heal'], captureNoisy: true,
|
||||
units,
|
||||
});
|
||||
h.step();
|
||||
h.b.players[0].dp = dp;
|
||||
const u = h.unit(1);
|
||||
const devs = h.b.allyUnits.filter((a) => a.kind === 'token' && a.defId === TOK[skill]).sort((a, b) => a.uid - b.uid);
|
||||
return { h, u, devs };
|
||||
}
|
||||
const st = (u) => u.mem.craftsman;
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Force every rng.chance roll to `v` (returns the probabilities asked for). */
|
||||
function forceChance(h, v) {
|
||||
const asked = [];
|
||||
h.b.rng.chance = (p) => { asked.push(p); return v; };
|
||||
return asked;
|
||||
}
|
||||
|
||||
test('白铁 in every 自选 form: his kit (all three skills authored), stats + module attributes, 1-1, blocks 2, melee physical ground-only, 维多利亚, no 特质, the data triggers', () => {
|
||||
assert.equal(OPERATOR_KITS[IRON], KITS[IRON]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null;
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [IRON, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.aspd + (m?.attr.aspd ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [2, 'melee', false, 'phys', 'craftsman', 1.5], `${label(f)}: 工匠`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr],
|
||||
[{ atk: 25, def: 25, aspd: 5 }, { atk: 45, def: 45, aspd: 5 }, { maxHp: 260, def: 45 }, { maxHp: 400, def: 60 }]);
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.deepEqual(diyRecordOf(CHESS[SLOT[5]], { charId: IRON, skillIndex: 0 }, data).bonds, ['victoriaShip']);
|
||||
assert.ok(KITTED_CHARS.includes(IRON));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(IRON), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: IRON, skillIndex: 2, uniEquipId: Y } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: IRON, skillIndex: 2, uniEquipId: Y } } });
|
||||
});
|
||||
|
||||
test('<支援装置> hand pieces: the picked skill\'s device only (variant sources), deploy limit 1 in the data (official 1 + max_deploy_count 1 = 2: reported), the module costs / redeploy times', () => {
|
||||
for (const [skill, id] of TOK.entries()) {
|
||||
for (const s of [0, 1, 2]) assert.equal(tokOf(id, 6, true, s, null).sources.includes('skill'), s === skill, `${id} under S${s + 1}`);
|
||||
const v = tokOf(id, 6, true, skill, null);
|
||||
assert.equal(v.stats.deployLimit, 1, `${id}: the data's deploy limit`);
|
||||
assert.equal(v.talents.find((t) => t.bb.max_deploy_count != null).bb.max_deploy_count, 1, `${id}: the hidden max_deploy_count (E2 "最多可部署2个")`);
|
||||
}
|
||||
// CRA-X 部署费用 −1 / −1 / −4, CRA-Y 再部署时间 −5 / −5 / −10 (PRTS module page)
|
||||
const cost = (mod) => TOK.map((id, s) => tokOf(id, 6, true, s, mod).stats.cost);
|
||||
const resp = (mod) => TOK.map((id, s) => tokOf(id, 6, true, s, mod).stats.respawnTime);
|
||||
assert.deepEqual([cost(null), cost(X), cost(Y)], [[3, 3, 10], [2, 2, 6], [3, 3, 10]]);
|
||||
assert.deepEqual([resp(null), resp(X), resp(Y)], [[10, 10, 20], [10, 10, 20], [5, 5, 10]]);
|
||||
});
|
||||
|
||||
test('战地工程师 / the device rules: stock 3 (CRA-X 4) after his deployment; a device that left comes back on its tile after its redeploy time, paying its cost and one stock; none without stock or DP; his devices leave with him and return after him', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [5, true, X], [6, true, Y]]) {
|
||||
const f = label([tier, elite, mod]);
|
||||
const { h, u, devs } = field({ tier, elite, mod, skill: 0, pieces: [[11, 7]], dp: 99 });
|
||||
const d = devs[0];
|
||||
const v = tokOf(P1, tier, elite, 0, mod);
|
||||
const cap = 3 + (mod === X ? 1 : 0);
|
||||
assert.deepEqual([st(u).stock, st(u).cap, d.alive, d.tileR, d.tileC], [cap, cap, true, 11, 7], `${f}: deployed for free with the board, stock ${cap}`);
|
||||
assert.deepEqual([d.base.cost, d.base.respawnTime], [v.stats.cost, v.stats.respawnTime], `${f}: the variant's cost / redeploy time`);
|
||||
assert.ok(d.s.flags.untargetable && d.s.flags.invulnerable && d.s.flags.noHeal, `${f}: 不会受到攻击, 无敌, 禁疗`);
|
||||
forceChance(h, false);
|
||||
h.b.kill(d, null);
|
||||
assert.equal(d.alive, false);
|
||||
h.run(v.stats.respawnTime - 0.3);
|
||||
assert.equal(d.alive, false, `${f}: not before its redeploy time`);
|
||||
h.run(0.6);
|
||||
assert.deepEqual([d.alive, d.tileR, d.tileC, st(u).stock, h.b.players[0].dp], [true, 11, 7, cap - 1, 99 - v.stats.cost], `${f}: back on its tile, −${v.stats.cost} DP, −1 stock`);
|
||||
// no stock ⇒ it waits
|
||||
st(u).stock = 0;
|
||||
h.b.kill(d, null);
|
||||
h.run(v.stats.respawnTime + 2);
|
||||
assert.equal(d.alive, false, `${f}: no stock, no return`);
|
||||
st(u).stock = 1;
|
||||
h.run(0.5);
|
||||
assert.ok(d.alive, `${f}: back as soon as he holds one`);
|
||||
// no DP ⇒ it waits
|
||||
st(u).stock = 1;
|
||||
h.b.players[0].dp = 0;
|
||||
h.b.kill(d, null);
|
||||
h.run(v.stats.respawnTime + 2);
|
||||
assert.equal(d.alive, false, `${f}: no DP, no return`);
|
||||
h.b.players[0].dp = 50;
|
||||
h.run(0.5);
|
||||
assert.ok(d.alive, `${f}: back once the DP is there`);
|
||||
// he leaves ⇒ his devices go; his return refills the stock and brings them back
|
||||
st(u).stock = 0;
|
||||
h.b.kill(u, null);
|
||||
assert.equal(d.alive, false, `${f}: gone with him (die_to_kill_token)`);
|
||||
h.b.players[0].dp = 99;
|
||||
assert.ok(h.runUntil(() => u.alive, 200), `${f}: he redeploys`);
|
||||
assert.equal(st(u).stock, cap, `${f}: stock refilled (charge_token[born])`);
|
||||
assert.ok(h.runUntil(() => d.alive, 30), `${f}: the device back after him`);
|
||||
assert.equal(st(u).stock, cap - 1);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('节约经费: a device of his leaving his x-4 ⇒ prob (0.7; CRA-X stage 3 0.9) ⇒ +1 stock (capped); none when the roll fails, outside his x-4 or when it goes with him', () => {
|
||||
for (const [tier, elite, mod, prob] of [[5, false, null, 0.7], [5, true, X, 0.7], [6, true, X, 0.9], [6, true, Y, 0.7]]) {
|
||||
const f = label([tier, elite, mod]);
|
||||
const { h, u, devs } = field({ tier, elite, mod, skill: 0, pieces: [[11, 6], [10, 8]], dp: 99 });
|
||||
const [near, far] = devs;
|
||||
const cap = st(u).cap;
|
||||
st(u).stock = 1;
|
||||
let asked = forceChance(h, true);
|
||||
h.b.kill(near, null);
|
||||
assert.deepEqual([asked, st(u).stock], [[prob], 2], `${f}: on his x-4 — rolled ${prob}, +1`);
|
||||
asked = forceChance(h, true);
|
||||
h.b.kill(far, null);
|
||||
assert.deepEqual([asked, st(u).stock], [[], 2], `${f}: two tiles away — no roll`);
|
||||
h.run(25);
|
||||
assert.ok(near.alive && far.alive);
|
||||
st(u).stock = 1;
|
||||
forceChance(h, false);
|
||||
h.b.kill(near, null);
|
||||
assert.equal(st(u).stock, 1, `${f}: a failed roll`);
|
||||
h.run(15);
|
||||
st(u).stock = cap;
|
||||
forceChance(h, true);
|
||||
h.b.kill(near, null);
|
||||
assert.equal(st(u).stock, cap, `${f}: capped at ${cap}`);
|
||||
h.run(15);
|
||||
st(u).stock = 0;
|
||||
asked = forceChance(h, true);
|
||||
h.b.kill(u, null);
|
||||
assert.deepEqual([asked, st(u).stock], [[], 0], `${f}: leaving with him — no roll`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 极致火力 + 白铁™多功能平台: ATK +12 % on the operator it faces (×2.5 / ×3 while S1 runs); S1 16 / 17 s, 140 % / 170 % ATK attacks, +1 stock at the start, every device destroyed at its end', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
const v = tokOf(P1, tier, elite, 0, null);
|
||||
const { h, u, devs } = field({ tier, elite, skill: 0, pieces: [[11, 4], [9, 8]], others: [{ uid: 3, chessId: 'chess_char_1_08_a', row: 11, col: 5 }] });
|
||||
const tx = h.unit(3);
|
||||
const [d] = devs;
|
||||
h.run(0.2);
|
||||
assert.deepEqual([u.skill.rule, sk.duration, sk.bb['attack@atk_scale'], sk.bb.fake_scale, v.skill.bb.talent_scale, v.talents[0].bb.atk], ['DEFAULT', elite ? 17 : 16, elite ? 1.7 : 1.4, elite ? 3 : 2.5, elite ? 3 : 2.5, 0.12], `T${tier}`);
|
||||
assert.deepEqual(tx.findBuff(`ironmn:pile1:${d.id}`)?.mods, { atkPct: 0.12 }, `T${tier}: 支援火力 on the operator it faces`);
|
||||
approx(tx.s.atk, tx.base.atk * 1.12, `T${tier}: ATK +12 %`);
|
||||
assert.equal(u.findBuff(`ironmn:pile1:${d.id}`), null, `T${tier}: not on 白铁 (not on its range)`);
|
||||
st(u).stock = 1;
|
||||
forceChance(h, false);
|
||||
u.skill.gainSp(999);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in his range`);
|
||||
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at the start`);
|
||||
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.02);
|
||||
h.step(2);
|
||||
approx(tx.s.atk, tx.base.atk * (1 + 0.12 * sk.bb.fake_scale), `T${tier}: ×${sk.bb.fake_scale} while S1 runs`);
|
||||
const n0 = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e).length;
|
||||
h.run(3);
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e).slice(n0);
|
||||
assert.ok(hits.length >= 1);
|
||||
for (const c of hits) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'], `T${tier}: ${sk.bb['attack@atk_scale'] * 100} % ATK`);
|
||||
const alive0 = devs.filter((x) => x.alive).length;
|
||||
assert.equal(alive0, 2);
|
||||
h.runUntil(() => !u.skill.active, sk.duration + 1);
|
||||
h.step();
|
||||
assert.deepEqual(devs.map((x) => x.alive), [false, false], `T${tier}: 技能结束时所有场上的装置被销毁`);
|
||||
assert.equal(tx.findBuff(`ironmn:pile1:${d.id}`), null, `T${tier}: its buff gone with it`);
|
||||
assert.ok(h.runUntil(() => devs.every((x) => x.alive), 30), `T${tier}: both back (stock 2)`);
|
||||
approx(tx.s.atk, tx.base.atk * 1.12, `T${tier}: ×1 after the skill`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 高效补给 + 白铁™多功能平台: +1 SP every 3.5 s to the operator it faces (2.5 / 2 s while S2 runs), the device loses 0.4 % (0.8 %) max HP / s; S2 30 s, ATK / DEF +25 / 40 %, every blocked enemy at once, +1 stock at its end', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
const v = tokOf(P2, tier, elite, 1, null);
|
||||
const { h, u, devs } = field({ tier, elite, skill: 1, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
|
||||
const yak = h.unit(3);
|
||||
const [d] = devs;
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.def, v.talents[0].bb['default.interval'], v.skill.bb['s2.interval'], v.talents.find((t) => t.bb.hp_ratio).bb.hp_ratio, v.skill.bb['s2.hp_ratio']],
|
||||
[30, elite ? 0.4 : 0.25, elite ? 0.4 : 0.25, 3.5, elite ? 2 : 2.5, 0.004, 0.008], `T${tier}`);
|
||||
const gifts = () => h.hooksOf('spGain').filter((c) => c.unit === yak && c.reason === 'device').map((c) => c.t);
|
||||
yak.skill.sp = 0;
|
||||
h.run(11);
|
||||
const g = gifts();
|
||||
assert.equal(g.length, 3, `T${tier}: 3 SP gifts in 11 s`);
|
||||
approx(g[1] - g[0], 3.5, `T${tier}: every 3.5 s`, 0.02);
|
||||
approx(d.hp, d.s.maxHp * (1 - 0.004 * Math.floor(h.b.time)), `T${tier}: 0.4 % / s`, 0.01);
|
||||
// S2: two blocked enemies at once, ATK / DEF, interval and bleed
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
const b = h.spawn('enemy_walk', { routeIndex: 0 });
|
||||
void b;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||||
approx(u.s.def, u.base.def * (1 + sk.bb.def), `T${tier}: DEF`);
|
||||
assert.deepEqual(u.skill.spec.attack, { hitAllBlocked: true }, `T${tier}: 攻击所有阻挡的敌人`);
|
||||
const hp0 = d.hp, t0 = h.b.time;
|
||||
yak.skill.sp = 0;
|
||||
const n0 = gifts().length;
|
||||
h.run(10.05);
|
||||
const g2 = gifts().slice(n0);
|
||||
approx(g2[1] - g2[0], v.skill.bb['s2.interval'], `T${tier}: every ${v.skill.bb['s2.interval']} s while S2 runs`, 0.02);
|
||||
approx(hp0 - d.hp, d.s.maxHp * 0.008 * Math.floor(h.b.time - t0 + 1e-9), `T${tier}: 0.8 % / s while S2 runs`, 0.05);
|
||||
void a;
|
||||
st(u).stock = 1;
|
||||
h.runUntil(() => !u.skill.active, 25);
|
||||
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at its end`);
|
||||
done(h);
|
||||
}
|
||||
// S2 hits every enemy it blocks
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 1, row: 9, col: 5 });
|
||||
h.spawn('enemy_walk', { routeIndex: 0 });
|
||||
h.run(0.4);
|
||||
h.spawn('enemy_walk', { routeIndex: 0 });
|
||||
assert.ok(h.runUntil(() => u.blocking.length === 2, 40), 'two blocked');
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
h.run(4);
|
||||
const ids = new Set(h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.dmg?.isAttack).map((c) => c.target.id));
|
||||
assert.equal(ids.size, 2, 'both blocked enemies hit');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 铁钳号·原型机: 30 s, ATK +25 / 40 %, ASPD +25 / 40, +1 stock at the start; the 铁钳号 is our attacks\' last target (an enemy first), takes no damage, +1 SP per hit, a 工匠\'s hit heals it 1 %; enemies never target it; immune to the PRTS statuses', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const v = tokOf(CRAB, tier, elite, 2, null);
|
||||
// the crab faces 白铁's front tile; 德克萨斯 (UP) has it on her range
|
||||
const { h, u, devs } = field({ tier, elite, skill: 2, pieces: [[10, 6]], others: [{ uid: 3, chessId: 'chess_char_1_08_a', row: 9, col: 6, dir: 'UP' }] });
|
||||
const tx = h.unit(3);
|
||||
const [c] = devs;
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.attack_speed, v.skill.spCost, v.skill.spType, v.skill.bb.hp_ratio, v.trait.bb.hp_ratio, v.stats.tauntLevel],
|
||||
[30, elite ? 0.4 : 0.25, elite ? 40 : 25, 5, 'INCREASE_WHEN_TAKEN_DAMAGE', 0.025, 0.01, -2], `T${tier}`);
|
||||
assert.ok(h.b.isAllyTarget(c), `T${tier}: an ally target`);
|
||||
assert.ok(c.s.flags.untargetable && c.s.flags.isolated && c.s.flags.noHeal && c.s.flags.noDisplace && !c.s.flags.invulnerable, `T${tier}: 不会受到攻击 (enemies), 孤立, 禁疗, 失衡免疫, not 无敌`);
|
||||
h.run(3);
|
||||
const hitsBy = (src) => h.hooksOf('damaged').filter((x) => x.target === c && x.source === src).length;
|
||||
const atkOn = (src) => h.events.filter((e) => e[0] === 'atk' && e[1] === src.id && e[2] === c.id).length;
|
||||
assert.ok(atkOn(u) > 0 && atkOn(tx) > 0, `T${tier}: 白铁 and 德克萨斯 attack it (no enemy around)`);
|
||||
assert.equal(hitsBy(u) + hitsBy(tx), 0, `T${tier}: no damage reaches it`);
|
||||
assert.equal(c.hp, c.s.maxHp);
|
||||
assert.equal(c.skill.sp, 5, `T${tier}: 5 SP (full) after 3 s of hits — 受击回复`);
|
||||
assert.ok(h.hooksOf('spGain').filter((x) => x.unit === c && x.reason === 'hurt').length >= 5, `T${tier}: one SP per hit`);
|
||||
// an enemy in an attacker's range comes first (one on the crab's tile, in 白铁's range)
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
h.step();
|
||||
const a0 = atkOn(u);
|
||||
h.run(3);
|
||||
assert.ok(h.events.filter((x) => x[0] === 'atk' && x[1] === u.id && x[2] === e.id).length >= 1, `T${tier}: 白铁 attacks the enemy`);
|
||||
assert.equal(atkOn(u), a0, `T${tier}: not the crab meanwhile`);
|
||||
h.b.kill(e, null);
|
||||
h.step();
|
||||
// heal from a 工匠's hit only
|
||||
c.hp = c.s.maxHp * 0.5;
|
||||
const healBy = () => h.hooksOf('heal').filter((x) => x.target === c).length;
|
||||
const h0 = healBy();
|
||||
h.b.dealDamage(u, c, { amount: 100, type: 'phys', isAttack: true });
|
||||
approx(c.hp, c.s.maxHp * 0.51, `T${tier}: 白铁 heals it 1 %`);
|
||||
h.b.dealDamage(tx, c, { amount: 100, type: 'phys', isAttack: true });
|
||||
approx(c.hp, c.s.maxHp * 0.51, `T${tier}: 德克萨斯 does not`);
|
||||
assert.equal(healBy() - h0, 1);
|
||||
// statuses PRTS lists, element, enemies' attacks
|
||||
for (const s of ['stun', 'cold', 'freeze', 'levitate', 'bind', 'silence', 'sleep']) assert.equal(h.b.applyStatus(c, s, { duration: 3, source: e }), false, `T${tier}: ${s} immune`);
|
||||
// S3 itself (an enemy in his range: the data's DEFAULT)
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
st(u).stock = 1;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||||
assert.equal(u.s.aspd, u.base.aspd + sk.bb.attack_speed, `T${tier}: ASPD`);
|
||||
approx(u.skill.timeLeft, 30, `T${tier}: 30 s`, 0.05);
|
||||
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at the start`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('铁钳号·原型机\'s skill (5 SP, an enemy in its 3-13, flyers too): −2.5 % max HP (流失), then 白铁\'s ATK × 230 % / 260 % physical on the first enemy and × 110 % / 120 % on every other within 1.1 of it; 团结的力量: the operator behind it takes ×0.8 physical (not arts)', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const v = tokOf(CRAB, tier, elite, 2, null);
|
||||
const { h, u, devs } = field({ tier, elite, skill: 2, row: 12, col: 3, pieces: [[10, 5]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 10, col: 4 }] });
|
||||
const yak = h.unit(3);
|
||||
const [c] = devs;
|
||||
assert.deepEqual([v.skill.bb.atk_scale, v.skill.bb.atk_scale_2, v.talents[0].bb.damage_resistance], [elite ? 2.6 : 2.3, elite ? 1.2 : 1.1, 0.2], `T${tier}`);
|
||||
assert.deepEqual(yak.findBuff(`ironmn:crab:${c.id}`)?.mods, { physTakenMul: 0.8 }, `T${tier}: 角峰 behind it`);
|
||||
// damage taken by 角峰: physical ×0.8, arts unchanged
|
||||
const p = h.b.dealDamage(null, yak, { amount: 1000, type: 'phys', canDodge: false, sourceless: true });
|
||||
const ar = h.b.dealDamage(null, yak, { amount: 1000, type: 'arts', canDodge: false, sourceless: true });
|
||||
approx(p, Math.max(1000 - yak.s.def, 50) * 0.8, `T${tier}: physical ×0.8`, 1e-3);
|
||||
approx(ar, 1000 * (1 - yak.s.res / 100), `T${tier}: arts ×1`, 1e-3);
|
||||
yak.hp = yak.s.maxHp;
|
||||
// its skill: an enemy in the 3-13, SP full
|
||||
const main = h.spawn('enemy_fly', { pos: [10, 7] });
|
||||
const near = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const far = h.spawn('enemy_dummy', { pos: [12, 8] });
|
||||
c.skill.gainSp(99);
|
||||
const atk = u.s.atk;
|
||||
const hp0 = c.hp;
|
||||
assert.ok(h.runUntil(() => c.skill.activations === 1, 1), `T${tier}: cast at full SP with an enemy in range`);
|
||||
approx(hp0 - c.hp, c.s.maxHp * 0.025, `T${tier}: −2.5 % max HP`);
|
||||
h.run(1);
|
||||
const by = (t) => h.hooksOf('damaged').filter((x) => x.source === c && x.target === t);
|
||||
assert.equal(by(main).length, 1, `T${tier}: the flyer is the first target (可对空)`);
|
||||
approx(by(main)[0].amount, atk * v.skill.bb.atk_scale, `T${tier}: ${v.skill.bb.atk_scale * 100} % of 白铁's ATK`);
|
||||
assert.equal(by(near).length, 1, `T${tier}: the enemy within 1.1`);
|
||||
approx(by(near)[0].amount, atk * v.skill.bb.atk_scale_2, `T${tier}: ${v.skill.bb.atk_scale_2 * 100} %`);
|
||||
assert.equal(by(far).length, 0, `T${tier}: farther than 1.1 untouched`);
|
||||
assert.deepEqual([by(main)[0].type, by(main)[0].dmg.isSkill], ['phys', true]);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('CRA-X: stage 3 节约经费 "当白铁周围8格存在自身装置时技力回复速度+0.2/秒" (one effect) — none at stage 1, none with the device outside his x-4', () => {
|
||||
for (const [tier, mod, want] of [[5, X, 0], [6, X, 0.2], [6, Y, 0], [6, null, 0]]) {
|
||||
const f = label([tier, true, mod]);
|
||||
const { h, u, devs } = field({ tier, elite: true, mod, skill: 0, pieces: [[11, 6], [9, 4]] });
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, u.base.spRecovery + want, `${f}: SP recovery`);
|
||||
if (want) {
|
||||
assert.deepEqual(u.findBuff('talent:ironmn:spRecover').mods, { spRecoveryFlat: 0.2 });
|
||||
for (const d of devs) h.b.retreat(d, { reason: 'expired' });
|
||||
h.step();
|
||||
assert.equal(u.s.spRecovery, u.base.spRecovery, `${f}: none once no device stands on his x-4`);
|
||||
}
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('CRA-Y: stage 3 "自身装置天赋生效的干员攻击速度+6(此效果不受技能影响)" — the operator a 平台 faces / the one behind a 铁钳号, +6 also while S1 runs; none at stage 1', () => {
|
||||
for (const [tier, want] of [[5, 0], [6, 6]]) {
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const f = `T${tier} CRA-Y S${skill + 1}`;
|
||||
const piece = skill === 2 ? [10, 7] : [11, 4];
|
||||
const op = skill === 2 ? { uid: 3, chessId: 'chess_char_1_08_a', row: 10, col: 6 } : { uid: 3, chessId: 'chess_char_1_08_a', row: 11, col: 5 };
|
||||
const { h, devs } = field({ tier, elite: true, mod: Y, skill, pieces: [piece], others: [op] });
|
||||
const tx = h.unit(3);
|
||||
h.step();
|
||||
assert.equal(tx.s.aspd, tx.base.aspd + want, `${f}: ASPD`);
|
||||
if (want) assert.deepEqual(tx.findBuff(`ironmn:aspd:${devs[0].id}`).mods, { aspd: 6 });
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: Y, skill: 0, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_08_a', row: 11, col: 5 }] });
|
||||
const tx = h.unit(3);
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
h.step();
|
||||
assert.equal(tx.s.aspd, tx.base.aspd + 6, 'not scaled by S1');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('铁钳号·原型机: enemies never select it (a ranged enemy beside it shoots 白铁 instead); its own HP loss can knock it out — then 节约经费 rolls and it returns like any device (20 s, CRA-Y 10 s; 10 DP, CRA-X 6)', () => {
|
||||
const shooter = enemyRec({ key: 'enemy_shooter', hp: 1e9, speed: 0, mass: 0, range: 2.5, atk: 50, bat: 1 });
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, X], [6, true, Y]]) {
|
||||
const f = label([tier, elite, mod]);
|
||||
const units = [
|
||||
{ uid: 1, diy: { slot: SLOT[tier], charId: IRON, skillIndex: 2, uniEquipId: mod }, elite, row: 10, col: 5 },
|
||||
{ uid: 10, kind: 'token', tokenId: CRAB, ownerUid: 1, row: 11, col: 6 },
|
||||
];
|
||||
const h = makeBattle({ defs: { enemies: { ...ENEMIES, enemy_shooter: shooter } }, timeLimit: 900, autoFinish: false, seed: 7, flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'death'], captureNoisy: true, units });
|
||||
h.step();
|
||||
h.b.players[0].dp = 99;
|
||||
const u = h.unit(1);
|
||||
const c = h.b.allyUnits.find((a) => a.defId === CRAB);
|
||||
const e = h.spawn('enemy_shooter', { pos: [9, 7] });
|
||||
h.run(5);
|
||||
const atk = h.events.filter((x) => x[0] === 'atk' && x[1] === e.id);
|
||||
assert.ok(atk.length > 0 && atk.every((x) => x[2] !== c.id), `${f}: the enemy shoots, never at the crab`);
|
||||
assert.equal(c.hp, c.s.maxHp, `${f}: untouched`);
|
||||
// knocked out by its own HP loss
|
||||
h.b.kill(e, null);
|
||||
h.step();
|
||||
const v = tokOf(CRAB, tier, elite, 2, mod);
|
||||
c.hp = 10;
|
||||
c.skill.gainSp(99);
|
||||
const asked = forceChance(h, true);
|
||||
h.spawn('enemy_dummy', { pos: [11, 8] });
|
||||
assert.ok(h.runUntil(() => !c.alive, 2), `${f}: its −2.5 % knocks it out`);
|
||||
assert.deepEqual(asked.length, 1, `${f}: 节约经费 rolled (on his x-4)`);
|
||||
const t0 = h.b.time;
|
||||
assert.ok(h.runUntil(() => c.alive, v.stats.respawnTime + 2), `${f}: back`);
|
||||
approx(h.b.time - t0, v.stats.respawnTime, `${f}: after ${v.stats.respawnTime} s`, 0.3);
|
||||
approx(h.b.players[0].dp, 99 - v.stats.cost, `${f}: −${v.stats.cost} DP`);
|
||||
assert.equal(c.hp, c.s.maxHp, `${f}: at full HP`);
|
||||
void u;
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
// test/content/op_nasti.test.js — the 自选 operator kit of 娜斯提 (char_4212_nasti, 6★ 工匠; kit
|
||||
// server/sim/content/kits/ops/op-nasti.js) and of her <支援装置> “质检专员” (token_10059_nasti_nstdef), “监工专员”
|
||||
// (token_10060_nasti_nstchr) and “应急承重小组” (token_10061_nasti_nstbld: the 小工程师 and its 高台), fielded the production way (a
|
||||
// DIY slot + its `diy` pick; the devices as the placed hand pieces of her player) in every form: tiers 5 / 6, normal (E2 Lv1,
|
||||
// skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or CRA-X 工程师们 at stage 1 (tier 5) / 3 (tier 6). Numbers
|
||||
// from data/backups.json; the fidelity checklist of kits/README.md.
|
||||
// Run: node --test test/content/op_nasti.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const NASTI = 'char_4212_nasti';
|
||||
const DEF = 'token_10059_nasti_nstdef', CHR = 'token_10060_nasti_nstchr', BLD = 'token_10061_nasti_nstbld';
|
||||
const TOK = [DEF, CHR, BLD];
|
||||
const FORMS = BACKUPS.units[NASTI].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const X = 'uniequip_002_nasti';
|
||||
const S1 = 'skchr_nasti_1', S2 = 'skchr_nasti_2', S3 = 'skchr_nasti_3';
|
||||
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const tokOf = (id, tier, elite, skill, mod) => {
|
||||
let v = BACKUPS.tokens[id].variants[`${NASTI}@${statusOf(tier, elite)}`];
|
||||
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
|
||||
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
|
||||
return v;
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = {
|
||||
enemy_dummy: dummy('enemy_dummy'), enemy_rng: dummy('enemy_rng', { range: 2 }),
|
||||
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }),
|
||||
enemy_hit: enemyRec({ key: 'enemy_hit', hp: 1e9, speed: 0.6, mass: 0, atk: 400, bat: 1 }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], [5, true, null], [5, true, X], [6, true, null], [6, true, X]];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, dir = 'RIGHT', pieces = [], others = [], seed = 5, dp = 99 } = {}) {
|
||||
const units = [
|
||||
{ uid: 1, diy: { slot: SLOT[tier], charId: NASTI, skillIndex: skill, uniEquipId: mod }, elite, row, col, dir },
|
||||
...pieces.map(([r, c, d], i) => ({ uid: 10 + i, kind: 'token', tokenId: TOK[skill], ownerUid: 1, row: r, col: c, dir: d ?? 'RIGHT' })),
|
||||
...others,
|
||||
];
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'death', 'deploy', 'spGain', 'heal'], captureNoisy: true,
|
||||
units,
|
||||
});
|
||||
h.step();
|
||||
h.b.players[0].dp = dp;
|
||||
const u = h.unit(1);
|
||||
const devs = h.b.allyUnits.filter((a) => a.kind === 'token' && a.defId === TOK[skill] && a.uid != null).sort((a, b) => a.uid - b.uid);
|
||||
return { h, u, devs };
|
||||
}
|
||||
const st = (u) => u.mem.craftsman;
|
||||
const platformOf = (u) => u.mem.nastiPlatform ?? null;
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
const protectOf = (a) => a.findBuff('protect')?.mods ?? null;
|
||||
|
||||
test('娜斯提 in every 自选 form: her kit (all three skills authored), stats + module attributes, 1-1, blocks 2, melee physical ground-only, 协防 (no faction bond), no 特质, the data triggers; offered as a pick', () => {
|
||||
assert.equal(OPERATOR_KITS[NASTI], KITS[NASTI]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null;
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [NASTI, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [2, 'melee', false, 'phys', 'craftsman', 1.5], `${label(f)}: 工匠`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr], [{ maxHp: 78, atk: 26, def: 25 }, { maxHp: 145, atk: 40, def: 45 }]);
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(NASTI));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(NASTI), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: NASTI, skillIndex: 1, uniEquipId: X } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[5]]: { charId: NASTI, skillIndex: 1, uniEquipId: X } } });
|
||||
});
|
||||
|
||||
test('<支援装置> hand pieces: the picked skill\'s device (variant sources), 2 of them (deploy limit 2, "最多可部署2个"); CRA-X costs 2 / 2 / 3; stock 3 (CRA-X 4); CRA-X stage 3: the first device deployment after each of hers costs 3 less', () => {
|
||||
for (const [skill, id] of TOK.entries()) {
|
||||
for (const s of [0, 1, 2]) assert.equal(tokOf(id, 6, true, s, null).sources.includes('skill'), s === skill, `${id} under S${s + 1}`);
|
||||
assert.equal(tokOf(id, 6, true, skill, null).stats.deployLimit, 2, `${id}: 2 hand pieces`);
|
||||
}
|
||||
assert.deepEqual(TOK.map((id, s) => tokOf(id, 6, true, s, null).stats.cost), [4, 4, 6]);
|
||||
assert.deepEqual(TOK.map((id, s) => tokOf(id, 6, true, s, X).stats.cost), [2, 2, 3]);
|
||||
assert.deepEqual(TOK.map((id, s) => tokOf(id, 6, true, s, null).stats.respawnTime), [8, 4, 6]);
|
||||
for (const [tier, elite, mod, cap, cut] of [[5, false, null, 3, 0], [5, true, X, 4, 0], [6, true, X, 4, 3]]) {
|
||||
const f = label([tier, elite, mod]);
|
||||
const { h, u, devs } = field({ tier, elite, mod, skill: 0, pieces: [[11, 7], [9, 7]] });
|
||||
const [a, b] = devs;
|
||||
assert.deepEqual([st(u).stock, st(u).cap, a.alive, b.alive], [cap, cap, true, true], `${f}: stock, both deployed free`);
|
||||
assert.equal(u.mem.nastiFirst, false, `${f}: the battle-start deployment used the first-device cut`);
|
||||
// she leaves and comes back: the first device back costs `cut` less, the next one its full cost
|
||||
h.b.kill(u, null);
|
||||
assert.ok(!a.alive && !b.alive, `${f}: her devices leave with her`);
|
||||
h.b.players[0].dp = 999;
|
||||
assert.ok(h.runUntil(() => u.alive, 200), `${f}: back`);
|
||||
h.b.players[0].dp = 100;
|
||||
assert.ok(h.runUntil(() => a.alive && b.alive, 20), `${f}: both devices back`);
|
||||
approx(h.b.players[0].dp, 100 - Math.max(0, a.base.cost - cut) - a.base.cost, `${f}: the first back ${Math.max(0, a.base.cost - cut)} DP, the second ${a.base.cost}`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('注意安全: 10 % 庇护 on her; once a 远程 enemy is on the field (until she leaves) 15 % on her and on our 远程 operators (not the 近战 ones), and +1 SP every 6 s for them', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const t1 = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
|
||||
assert.deepEqual([t1['nasti_t2[res].damage_resistance'], t1['nasti_t2[res_plus].damage_resistance'], t1['nasti_t2[add_sp].interval']], [0.1, 0.15, 6]);
|
||||
const { h, u } = field({ tier, elite, skill: 0, others: [{ uid: 3, chessId: 'chess_char_1_14_a', row: 9, col: 3 }, { uid: 4, chessId: 'chess_char_1_08_a', row: 12, col: 3 }] });
|
||||
const prov = h.unit(3), tx = h.unit(4);
|
||||
h.run(0.5);
|
||||
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 0.9, artsTakenMul: 0.9 }, null, null], `T${tier}: 10 % on her only`);
|
||||
h.spawn('enemy_dummy', { pos: [11, 9] });
|
||||
h.run(0.5);
|
||||
assert.deepEqual(protectOf(u), { physTakenMul: 0.9, artsTakenMul: 0.9 }, `T${tier}: a 近战 enemy changes nothing`);
|
||||
const rng = h.spawn('enemy_rng', { pos: [12, 9] });
|
||||
h.run(0.5);
|
||||
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 0.85, artsTakenMul: 0.85 }, { physTakenMul: 0.85, artsTakenMul: 0.85 }, null], `T${tier}: 15 % on her and 格雷伊 (远程), not 德克萨斯`);
|
||||
const gifts = (a) => h.hooksOf('spGain').filter((c) => c.unit === a && c.reason === 'talent').map((c) => c.t);
|
||||
prov.skill.sp = 0;
|
||||
u.skill.sp = 0;
|
||||
const g0 = gifts(prov).length;
|
||||
h.run(12.1);
|
||||
assert.equal(gifts(prov).length - g0, 2, `T${tier}: +1 SP every 6 s (格雷伊)`);
|
||||
assert.ok(gifts(u).length >= 2, `T${tier}: and her`);
|
||||
assert.equal(gifts(tx).length, 0, `T${tier}: not 德克萨斯`);
|
||||
h.b.kill(rng, null);
|
||||
h.run(1);
|
||||
assert.deepEqual(protectOf(prov), { physTakenMul: 0.85, artsTakenMul: 0.85 }, `T${tier}: permanent until she leaves`);
|
||||
h.b.kill(u, null);
|
||||
h.run(1);
|
||||
assert.equal(protectOf(prov), null, `T${tier}: gone with her`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 “拱卫” + “质检专员”: DEF +25 % on the operator it faces; while S1 runs DEF +40 / 46 %, block +1 and the device −2 % max HP / s; S1 38 / 40 s, ATK / DEF +52 / 65 %, every blocked enemy at once, +1 stock at the start', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
const v = tokOf(DEF, tier, elite, 0, null);
|
||||
const { h, u, devs } = field({ tier, elite, skill: 0, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
|
||||
const yak = h.unit(3);
|
||||
const [d] = devs;
|
||||
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.def, sk.bb['talent@def'], v.talents[1].bb.def, v.skill.bb['talent@block_cnt'], v.skill.bb['talent@hp_ratio']],
|
||||
[elite ? 40 : 38, elite ? 0.65 : 0.52, elite ? 0.65 : 0.52, elite ? 0.46 : 0.4, 0.25, 1, 0.02], `T${tier}`);
|
||||
h.step();
|
||||
assert.deepEqual(yak.findBuff(`nasti:nstdef:${d.id}`)?.mods, { defPct: 0.25 }, `T${tier}: 严禁偷工减料`);
|
||||
assert.ok(d.s.flags.untargetable && d.s.flags.invulnerable && d.s.flags.noHeal, `T${tier}: 不会受到攻击, 无敌, 禁疗`);
|
||||
st(u).stock = 1;
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
assert.equal(st(u).stock, 2, `T${tier}: +1 stock`);
|
||||
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.02);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||||
assert.deepEqual(u.skill.spec.attack, { hitAllBlocked: true });
|
||||
h.step();
|
||||
assert.deepEqual(yak.findBuff(`nasti:nstdef:${d.id}`)?.mods, { defPct: sk.bb['talent@def'], blockCnt: 1 }, `T${tier}: the S1 mode`);
|
||||
assert.equal(yak.s.blockCnt, yak.base.blockCnt + 1, `T${tier}: block +1`);
|
||||
const hp0 = d.hp;
|
||||
h.run(5.02);
|
||||
approx(hp0 - d.hp, d.s.maxHp * 0.02 * 5, `T${tier}: −2 % / s`, 1e-3);
|
||||
h.runUntil(() => !u.skill.active, sk.duration + 1);
|
||||
h.step();
|
||||
assert.deepEqual(yak.findBuff(`nasti:nstdef:${d.id}`)?.mods, { defPct: 0.25 }, `T${tier}: back to +25 %`);
|
||||
approx(d.hp, d.s.maxHp * (1 - 0.02 * (sk.duration - 1)), `T${tier}: ${sk.duration - 1} ticks of −2 % (at 1 … ${sk.duration - 1} s; the skill ends at ${sk.duration})`, 1e-3);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 “执行” + “监工专员”: deployed at 3 / 8 HP, +1 HP every 6 SP (阻回 at full HP); S2 4 s: no attack, a barrier of 6 / 8 % of her max HP each 0.5 s up to 48 / 64 %; the device: +1 SP and a 3 / 4 % barrier to the operator it faces each 0.5 s, −1 HP each, leaves at the end; +1 stock at the end; her barriers go when she leaves', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
const v = tokOf(CHR, tier, elite, 1, null);
|
||||
const { h, u, devs } = field({ tier, elite, skill: 1, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
|
||||
const yak = h.unit(3);
|
||||
const [d] = devs;
|
||||
assert.deepEqual([sk.duration, sk.bb['attack@nasti_s2[update_shield].shield_each_hp_rate'], sk.bb['attack@nasti_s2[update_shield].shield_max_hp_rate'], v.skill.bb['talent@shield_each_hp_rate'], v.skill.bb['talent@shield_max_hp_rate'], v.stats.maxHp, v.talents[1].bb.init_hp, v.skill.spCost],
|
||||
[4, elite ? 0.08 : 0.06, elite ? 0.64 : 0.48, elite ? 0.04 : 0.03, elite ? 0.64 : 0.48, 8, 3, 6], `T${tier}`);
|
||||
assert.deepEqual([d.hp, d.s.maxHp], [3, 8], `T${tier}: 3 / 8 HP`);
|
||||
h.run(12.2);
|
||||
assert.equal(d.hp, 5, `T${tier}: +1 HP every 6 s`);
|
||||
h.run(18.2);
|
||||
assert.equal(d.hp, 8, `T${tier}: full`);
|
||||
assert.ok(d.s.flags.noSp, `T${tier}: 阻回 at full HP`);
|
||||
// S2: 8 pulses over 4 s on 角峰; the device dies on its last HP; her own barrier
|
||||
st(u).stock = 1;
|
||||
yak.skill.sp = 0;
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
|
||||
assert.deepEqual(u.skill.spec.attack, { noAttack: true }, `T${tier}: 停止攻击`);
|
||||
const t0 = h.b.time;
|
||||
h.runUntil(() => !u.skill.active, 6);
|
||||
approx(h.b.time - t0, 4, `T${tier}: 4 s`, 0.03);
|
||||
const each = u.s.maxHp * v.skill.bb['talent@shield_each_hp_rate'];
|
||||
approx(yak.findBuff(`nasti:shield:${u.id}`)?.shield ?? 0, 8 * each, `T${tier}: 8 pulses of ${v.skill.bb['talent@shield_each_hp_rate'] * 100} % of her max HP`, 1e-3);
|
||||
assert.equal(h.hooksOf('spGain').filter((c) => c.unit === yak && c.reason === 'device').length, 8, `T${tier}: 8 SP`);
|
||||
approx(u.findBuff('nasti:s2shield').shield, u.s.maxHp * sk.bb['attack@nasti_s2[update_shield].shield_max_hp_rate'], `T${tier}: her barrier at the cap`, 1e-3);
|
||||
assert.equal(d.alive, false, `T${tier}: the device spent (8 HP) / left at the end`);
|
||||
assert.equal(st(u).stock, 2, `T${tier}: +1 stock at the end (the device not back yet)`);
|
||||
assert.ok(h.runUntil(() => d.alive, 10), `T${tier}: back after 4 s`);
|
||||
assert.deepEqual([d.hp, st(u).stock], [3, 1]);
|
||||
// she leaves: every barrier of her S2 goes
|
||||
h.b.kill(u, null);
|
||||
assert.ok(!yak.findBuff(`nasti:shield:${u.id}`) && !u.findBuff('nasti:s2shield'), `T${tier}: barriers gone with her`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 栖脚地 + 应急承重小组: the 小工程师 builds the 高台 on the free low tile it faces (none on a taken tile); 50 SP a level (its own 1 / s + 1 / s from the 小工程师, 2 / s in S3); level 1 attackable, block 1; level 2 +1000 HP / +100 DEF / +5 RES; level 3 +2000 / +200 / +10, 阻回; heals 1 % (2 %) for −2 %; stays when she leaves only at level 3; S3 15 s, ATK / DEF +100 / 130 %', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const v = tokOf(BLD, tier, elite, 2, null);
|
||||
const { h, u, devs } = field({ tier, elite, skill: 2, pieces: [[11, 4], [9, 4, 'UP']], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 10, col: 4 }] });
|
||||
const [eng, blocked] = devs;
|
||||
const p = platformOf(u);
|
||||
assert.ok(p && p.alive, `T${tier}: the 高台 built`);
|
||||
assert.deepEqual([p.tileR, p.tileC, p.mem.nastiLevel], [11, 5, 0], `T${tier}: on the tile it faces`);
|
||||
assert.equal(h.b.allyUnits.filter((a) => a.defId === BLD && a.mem.nastiPlatform).length, 1, `T${tier}: one 高台 (the other 小工程师 faces 角峰)`);
|
||||
void blocked;
|
||||
assert.ok(p.s.flags.untargetable && p.s.flags.invulnerable && p.s.blockCnt === 0, `T${tier}: 未激活 — untargetable, invulnerable, blocks nobody`);
|
||||
assert.ok(eng.s.flags.untargetable && eng.s.flags.invulnerable && eng.s.flags.noHeal, `T${tier}: the 小工程师 不会受到攻击`);
|
||||
assert.deepEqual([v.skill.spCost, v.talents[0].bb.m1_sp, v.talents[0].bb.m2_sp, v.talents[0].bb['nasti_nstbld_m1[heal_m456].hp_ratio'], v.talents[0].bb['nasti_nstbld_m2[heal_m456].hp_ratio']], [50, 1, 2, 0.01, 0.02], `T${tier}`);
|
||||
// level 1 after 25 s (1 + 1 SP / s)
|
||||
assert.ok(h.runUntil(() => p.mem.nastiLevel === 1, 27), `T${tier}: level 1`);
|
||||
approx(h.b.time, 25, `T${tier}: 50 SP at 2 / s`, 0.08);
|
||||
assert.ok(!p.s.flags.untargetable && !p.s.flags.invulnerable && p.s.blockCnt === 1, `T${tier}: 激活 — attackable, block 1`);
|
||||
// S3: 3 SP / s (1 own + 2), level 2
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK +${sk.bb.atk * 100} %`);
|
||||
approx(u.skill.timeLeft, 15, `T${tier}: 15 s`, 0.05);
|
||||
const sp0 = p.skill.sp, tS = h.b.time;
|
||||
h.run(5);
|
||||
approx(p.skill.sp - sp0, 3 * 5, `T${tier}: 3 SP / s in S3`, 0.05);
|
||||
void tS;
|
||||
assert.ok(h.runUntil(() => p.mem.nastiLevel === 2, 20), `T${tier}: level 2`);
|
||||
approx(p.s.maxHp, v.stats.maxHp + 1000, `T${tier}: +1000 HP`);
|
||||
assert.deepEqual([p.s.def, p.s.res], [v.stats.def + 100, v.stats.res + 5], `T${tier}: +100 DEF / +5 RES`);
|
||||
// heal: 1 % of its max HP for −2 % of the 小工程师's
|
||||
h.runUntil(() => !u.skill.active, 20);
|
||||
p.hp = p.s.maxHp * 0.5;
|
||||
const e0 = eng.hp, p0 = p.hp;
|
||||
h.run(1.01);
|
||||
approx(p.hp - p0, p.s.maxHp * 0.01, `T${tier}: heals 1 %`, 0.02);
|
||||
approx(e0 - eng.hp, eng.s.maxHp * 0.02, `T${tier}: −2 % own`, 0.02);
|
||||
// level 3: 阻回; stays when she leaves
|
||||
p.skill.gainSp(999);
|
||||
h.step(2);
|
||||
assert.equal(p.mem.nastiLevel, 3, `T${tier}: level 3`);
|
||||
approx(p.s.maxHp, v.stats.maxHp + 2000, `T${tier}: +2000 HP`);
|
||||
assert.ok(p.s.flags.noSp, `T${tier}: 阻回 at the top level`);
|
||||
h.b.kill(u, null);
|
||||
assert.ok(p.alive, `T${tier}: a level-3 高台 stays when she leaves`);
|
||||
assert.ok(!eng.alive, `T${tier}: the 小工程师 goes with her`);
|
||||
done(h);
|
||||
}
|
||||
// below level 3 the 高台 goes with her; no 高台 on a taken tile
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2, pieces: [[11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
|
||||
assert.equal(platformOf(u), null, 'the tile it faces is taken: no 高台');
|
||||
done(h);
|
||||
const r2 = field({ tier: 6, elite: true, skill: 2, pieces: [[11, 4]] });
|
||||
const p2 = platformOf(r2.u);
|
||||
r2.h.b.kill(r2.u, null);
|
||||
assert.ok(!p2.alive, 'a level-0 高台 goes with her');
|
||||
done(r2.h);
|
||||
});
|
||||
|
||||
test('CRA-X stage 3 "装置可与攻击范围内其他装置共同提供效果": a 质检专员 with another on its range also reaches that one\'s range (the operator ahead gets both) — not at stage 1 or without the module', () => {
|
||||
for (const [tier, mod, want] of [[6, X, 2], [5, X, 1], [6, null, 1]]) {
|
||||
const { h, devs } = field({ tier, elite: true, mod, skill: 0, pieces: [[11, 3], [11, 4]], others: [{ uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 5 }] });
|
||||
const yak = h.unit(3);
|
||||
h.step();
|
||||
const n = devs.filter((d) => yak.findBuff(`nasti:nstdef:${d.id}`)).length;
|
||||
assert.equal(n, want, `T${tier} ${mod ?? 'none'}: ${want} device(s) reach 角峰`);
|
||||
approx(yak.s.def, yak.base.def * (1 + 0.25 * want), `T${tier}: DEF +${25 * want} %`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,390 @@
|
||||
// test/content/op_necras.test.js — the 自选 operator kit of 死芒 (char_450_necras, 6★ 塑灵术师; kit
|
||||
// server/sim/content/kits/ops/op-necras.js) and of her summon 悲叹的仆役 (token_10043_necras_skeltn, and its special form),
|
||||
// fielded the production way (a DIY slot + its `diy` pick) in every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module)
|
||||
// and elite (E2 Lv60, rank 7) with no module or SOC-X 未处理的遗产 at stage 1 (tier 5) / 3 (tier 6). Numbers from
|
||||
// data/backups.json; the fidelity checklist of kits/README.md.
|
||||
// Run: node --test test/content/op_necras.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
|
||||
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
||||
const CHESS = load('chess');
|
||||
const BACKUPS = load('backups');
|
||||
const NEC = 'char_450_necras';
|
||||
const SKEL = 'token_10043_necras_skeltn';
|
||||
const FORMS = BACKUPS.units[NEC].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const X = 'uniequip_002_necras';
|
||||
const S1 = 'skchr_necras_1', S2 = 'skchr_necras_2', S3 = 'skchr_necras_3';
|
||||
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
|
||||
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
const skelOf = (tier, elite, skill) => {
|
||||
let v = BACKUPS.tokens[SKEL].variants[`${NEC}@${statusOf(tier, elite)}`];
|
||||
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
|
||||
return v;
|
||||
};
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const ENEMIES = {
|
||||
enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e9, speed: 0, mass: 0 }),
|
||||
enemy_weak: enemyRec({ key: 'enemy_weak', hp: 100, speed: 0, mass: 0 }),
|
||||
enemy_fly: enemyRec({ key: 'enemy_fly', hp: 1e9, speed: 0, mass: 0, motion: 'FLY' }),
|
||||
enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.5, mass: 0 }),
|
||||
};
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], [5, true, null], [5, true, X], [6, true, null], [6, true, X]];
|
||||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||||
|
||||
/** 死芒 as uid 1 at (10, 3) facing RIGHT: her 3-1 is rows 9–11 × cols 3–5 and (10, 6). */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 3, others = [], seed = 5 } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'death', 'deploy', 'statusApplied', 'heal'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: NEC, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const skels = (u) => (u.mem.necras?.skels ?? []).filter((s) => s.alive);
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
/** Knock an enemy out at `pos` (the kill is credited to nobody: anyone's kill counts). */
|
||||
function killAt(h, pos, key = 'enemy_weak') {
|
||||
const e = h.spawn(key, { pos });
|
||||
h.step();
|
||||
h.b.kill(e, null);
|
||||
return e;
|
||||
}
|
||||
|
||||
test('死芒 in every 自选 form: her kit (all three skills authored), stats + module attributes, 3-1, blocks 1, ranged arts that hit air, 维多利亚, no 特质, the data triggers (S3 SKILL_RANGE on its 3-3); offered as a pick', () => {
|
||||
assert.equal(OPERATOR_KITS[NEC], KITS[NEC]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u } = field({ tier, elite, mod, skill });
|
||||
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null;
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [NEC, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [1, 'ranged', true, 'arts', 1.6], `${label(f)}: 塑灵术师`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'SKILL_RANGE']);
|
||||
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr], [{ maxHp: 120, atk: 35 }, { maxHp: 200, atk: 55 }]);
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(NEC));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(NEC), `tier ${t}`);
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: NEC, skillIndex: 2, uniEquipId: X } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: NEC, skillIndex: 2, uniEquipId: X } } });
|
||||
});
|
||||
|
||||
test('复燃: a knock-out on her range (anyone\'s, stealth ignored) makes a 悲叹的仆役 on its tile 0.1 s later (facing as she does); off her range nothing; up to 3, then the upgrade (+100 % HP / ATK, +40 % DEF, block +1, healed to full; not yet upgraded first), all upgraded: the lowest healed; they leave with her', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const t0 = formOf(tier, elite).talents.find((t) => t.index === 0).bb;
|
||||
const v = skelOf(tier, elite, 0);
|
||||
assert.deepEqual([t0.max_token_cnt, t0.max_hp, t0.atk, t0.def, t0.block_cnt], [3, 1, 1, 0.4, 1], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
killAt(h, [10, 5]);
|
||||
assert.equal(skels(u).length, 0, `T${tier}: not at once`);
|
||||
h.run(0.11);
|
||||
const [a] = skels(u);
|
||||
assert.ok(a, `T${tier}: one 0.1 s later`);
|
||||
assert.deepEqual([a.tileR, a.tileC, a.dir, a.defId], [10, 5, u.dir, SKEL], `T${tier}: on the kill tile, facing as she does`);
|
||||
assert.deepEqual([a.base.maxHp, a.base.atk, a.base.def, a.s.blockCnt, a.profile.dmgType, a.profile.attack, a.profile.canHitFly], [v.stats.maxHp, v.stats.atk, v.stats.def, 1, 'arts', 'melee', false], `T${tier}: the token's stats, arts melee, ground only`);
|
||||
assert.deepEqual(a.liveRangeGrid, v.rangeGrid, `T${tier}: x-5`);
|
||||
assert.ok(a.s.flags.noHeal && a.s.flags.noSp && a.s.flags.silence, `T${tier}: 禁疗, 阻回, 沉默`);
|
||||
killAt(h, [10, 8]);
|
||||
h.run(0.2);
|
||||
assert.equal(skels(u).length, 1, `T${tier}: off her range — nothing`);
|
||||
killAt(h, [9, 4]);
|
||||
h.run(0.2);
|
||||
killAt(h, [11, 4]);
|
||||
h.run(0.2);
|
||||
assert.equal(skels(u).length, 3, `T${tier}: three`);
|
||||
const [, b, c] = skels(u);
|
||||
b.hp = b.s.maxHp * 0.5;
|
||||
c.hp = c.s.maxHp * 0.3;
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
assert.equal(skels(u).length, 3, `T${tier}: no 4th`);
|
||||
assert.ok(c.findBuff('necras:upgrade') && !a.findBuff('necras:upgrade') && !b.findBuff('necras:upgrade'), `T${tier}: the lowest-HP one upgraded`);
|
||||
assert.deepEqual(c.findBuff('necras:upgrade').mods, { hpPct: 1, atkPct: 1, defPct: 0.4, blockCnt: 1 });
|
||||
assert.deepEqual([c.hp, c.s.maxHp, c.s.blockCnt], [c.base.maxHp * 2, c.base.maxHp * 2, 2], `T${tier}: healed to full, block 2`);
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
assert.ok(skels(u).every((s) => s.findBuff('necras:upgrade')), `T${tier}: all three upgraded`);
|
||||
b.hp = 100;
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
assert.equal(b.hp, b.s.maxHp, `T${tier}: all upgraded — the lowest healed`);
|
||||
h.b.kill(u, null);
|
||||
assert.equal(skels(u).length, 0, `T${tier}: they leave with her`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('复燃: no deployable kill tile ⇒ a random free low melee tile of her range, those with an enemy first; no tile at all ⇒ the upgrade instead', () => {
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 1, others: [
|
||||
{ uid: 3, chessId: 'chess_char_1_02_a', row: 9, col: 3 }, { uid: 4, chessId: 'chess_char_1_02_a', row: 9, col: 4 }, { uid: 5, chessId: 'chess_char_1_02_a', row: 9, col: 5 },
|
||||
{ uid: 6, chessId: 'chess_char_1_02_a', row: 11, col: 3 }, { uid: 7, chessId: 'chess_char_1_02_a', row: 11, col: 4 }, { uid: 8, chessId: 'chess_char_1_02_a', row: 11, col: 5 },
|
||||
] });
|
||||
// her range: (9|10|11, 3–5) + (10, 6); free: (10, 4), (10, 5), (10, 6); an enemy stands on (10, 6)
|
||||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
// a kill on a taken tile (an operator on it) ⇒ the tile with the enemy
|
||||
killAt(h, [9, 4]);
|
||||
h.run(0.2);
|
||||
const [a] = skels(u);
|
||||
assert.deepEqual([a.tileR, a.tileC], [10, 6], 'the free tile with an enemy');
|
||||
killAt(h, [9, 5]);
|
||||
h.run(0.2);
|
||||
killAt(h, [9, 5]);
|
||||
h.run(0.2);
|
||||
assert.deepEqual(skels(u).map((s) => [s.tileR, s.tileC]).sort(), [[10, 4], [10, 5], [10, 6]], 'then the other free tiles');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('回光黯淡: her and her summons\' damage on an enemy below 50 % HP ×1.4 (the final damage); SOC-X "攻击力提升至115%" on an enemy her summon blocks (stage 1 and 3), which she can attack anywhere', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [5, true, X], [6, true, X]]) {
|
||||
const f = label([tier, elite, mod]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, row: 9, col: 3 });
|
||||
const e = h.spawn('enemy_dummy', { pos: [9, 5] });
|
||||
h.run(3);
|
||||
const hits = () => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.dmg.isAttack);
|
||||
const full = hits().at(-1).amount;
|
||||
approx(full, u.s.atk, `${f}: ×1 at full HP`);
|
||||
e.hp = e.s.maxHp * 0.49;
|
||||
const n0 = hits().length;
|
||||
h.run(2);
|
||||
approx(hits().slice(n0).at(-1).amount, u.s.atk * 1.4, `${f}: ×1.4 below 50 %`);
|
||||
done(h);
|
||||
}
|
||||
// the trait: she attacks the enemy her summon blocks out of her range (×1.15 with SOC-X)
|
||||
for (const [tier, elite, mod, mul] of [[5, false, null, 1], [5, true, X, 1.15], [6, true, X, 1.15]]) {
|
||||
const f = label([tier, elite, mod]);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, row: 12, col: 8 });
|
||||
// her range: rows 11–13 × cols 8–10 and (12, 11); a 悲叹的仆役 made on (11, 9) blocks a walker on row 11 coming from the right
|
||||
killAt(h, [11, 9]);
|
||||
h.run(0.2);
|
||||
const [s] = skels(u);
|
||||
assert.deepEqual([s.tileR, s.tileC], [11, 9]);
|
||||
const w = h.spawn('enemy_walk', { pos: [11, 13] });
|
||||
assert.ok(h.runUntil(() => w.blockedBy === s, 20), `${f}: blocked by her summon`);
|
||||
h.step(3);
|
||||
assert.ok(u.extraRangeKeys && u.extraRangeKeys.includes(Math.round(w.y) * 21 + Math.round(w.x)), `${f}: its tile is her target`);
|
||||
const n0 = h.hooksOf('damaged').filter((c) => c.source === u && c.target === w).length;
|
||||
h.run(4);
|
||||
const hit = h.hooksOf('damaged').filter((c) => c.source === u && c.target === w && c.dmg.isAttack).slice(n0);
|
||||
assert.ok(hit.length >= 1, `${f}: she attacks it`);
|
||||
approx(hit[0].amount, u.s.atk * mul, `${f}: ×${mul}`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('SOC-X stage 3: a knock-out on her range or her summons\' ranges ⇒ her ATK +25 % for 10 s (refreshed); one on a summon\'s range only also makes a 悲叹的仆役 (a random tile of her range) — none of it at stage 1', () => {
|
||||
for (const [tier, mod, on] of [[5, X, false], [6, X, true], [6, null, false]]) {
|
||||
const f = label([tier, true, mod]);
|
||||
const { h, u } = field({ tier, elite: true, mod, skill: 1, row: 10, col: 3 });
|
||||
// a summon on the tip of her range (10, 6): its x-5 reaches (10, 7), (9, 6), (11, 6) — outside hers
|
||||
killAt(h, [10, 6]);
|
||||
h.run(0.2);
|
||||
const [s] = skels(u);
|
||||
assert.deepEqual([s.tileR, s.tileC], [10, 6]);
|
||||
assert.equal(!!u.findBuff('necras:killAtk'), on, `${f}: ATK buff`);
|
||||
if (on) {
|
||||
assert.deepEqual(u.findBuff('necras:killAtk').mods, { atkPct: 0.25 });
|
||||
approx(u.findBuff('necras:killAtk').timeLeft, 10 - 0.2, `${f}: 10 s`, 0.02);
|
||||
h.run(5);
|
||||
killAt(h, [9, 4]);
|
||||
approx(u.findBuff('necras:killAtk').timeLeft, 10, `${f}: refreshed`, 0.02);
|
||||
h.run(0.2);
|
||||
}
|
||||
const n = skels(u).length;
|
||||
killAt(h, [10, 7]);
|
||||
h.run(0.2);
|
||||
assert.equal(skels(u).length, on ? n + 1 : n, `${f}: the summon-range kill makes one (stage 3 only)`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 噩愿: each 悲叹的仆役 made or upgraded strikes every enemy within 1.5 for her ATK × 310 % / 370 % arts (air units too); the cast re-makes them all — 1 per summon, 2 per upgraded one, 1 with none', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.bb.atk_scale, sk.bb.range_radius], [elite ? 3.7 : 3.1, 1.5]);
|
||||
const { h, u } = field({ tier, elite, skill: 0 });
|
||||
const near = h.spawn('enemy_fly', { pos: [11, 5] });
|
||||
const far = h.spawn('enemy_dummy', { pos: [12, 7] });
|
||||
killAt(h, [10, 5]);
|
||||
h.run(0.2);
|
||||
const by = (t) => h.hooksOf('damaged').filter((c) => c.source === u && c.target === t && c.dmg.tags?.includes('necras:s1'));
|
||||
assert.equal(by(near).length, 1, `T${tier}: the flyer within 1.5`);
|
||||
approx(by(near)[0].amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`);
|
||||
assert.equal(by(far).length, 0, `T${tier}: farther untouched`);
|
||||
// fill to 3 and upgrade one (another strike)
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
killAt(h, [11, 4]);
|
||||
h.run(0.2);
|
||||
const k0 = by(near).length;
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
assert.ok(skels(u).some((s) => s.findBuff('necras:upgrade')));
|
||||
assert.ok(by(near).length >= k0, `T${tier}: the upgrade strikes too`);
|
||||
// the cast: 2 + 1 + 1 = 4 credits ⇒ 3 summons + 1 upgrade
|
||||
const old = skels(u).map((s) => s.id);
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `T${tier}: cast`);
|
||||
h.run(0.5);
|
||||
const now = skels(u);
|
||||
assert.equal(now.length, 3, `T${tier}: three again`);
|
||||
assert.ok(now.every((s) => !old.includes(s.id)), `T${tier}: all re-made`);
|
||||
assert.equal(now.filter((s) => s.findBuff('necras:upgrade')).length, 1, `T${tier}: the 4th credit upgrades one`);
|
||||
done(h);
|
||||
}
|
||||
// none on the field ⇒ one
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 0 });
|
||||
h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4));
|
||||
h.run(0.3);
|
||||
assert.equal(skels(u).length, 1, 'none ⇒ one');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S2 折朽: 12 s; up to 2 enemies of her range sleep while linked and take her ATK × 100 % / 130 % arts every 0.5 s through it; no attack meanwhile; a linked one knocked out ⇒ +2 summons; every link broken ⇒ the skill ends and the sleep with it', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.duration, sk.bb.atk_scale, sk.bb.max_target, sk.bb.interval, sk.bb.additional_token_cnt], [12, elite ? 1.3 : 1, 2, 0.5, 2], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
const b = h.spawn('enemy_dummy', { pos: [9, 5] });
|
||||
const c = h.spawn('enemy_dummy', { pos: [11, 5] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
|
||||
const slept = [a, b, c].filter((e) => e.findBuff('sleep'));
|
||||
assert.equal(slept.length, 2, `T${tier}: two asleep`);
|
||||
assert.deepEqual(u.skill.spec.attack, { noAttack: true });
|
||||
const t0 = h.b.time;
|
||||
h.run(3.01);
|
||||
const ticks = h.hooksOf('damaged').filter((x) => x.source === u && slept.includes(x.target) && x.dmg.tags?.includes('necras:s2') && x.t > t0 - 1e-9);
|
||||
assert.equal(ticks.length, 12, `T${tier}: 6 ticks each in 3 s`);
|
||||
for (const x of ticks) approx(x.amount, u.s.atk * sk.bb.atk_scale * (1 - x.target.s.res / 100), `T${tier}: ${sk.bb.atk_scale * 100} % arts`, 1e-3);
|
||||
assert.ok(ticks.every((x) => x.target.findBuff('sleep')), `T${tier}: through the sleep`);
|
||||
// a linked one knocked out ⇒ +2 summons (and 复燃's own one on its tile)
|
||||
const n0 = skels(u).length;
|
||||
h.b.kill(slept[0], null);
|
||||
h.run(0.4);
|
||||
assert.equal(skels(u).length, n0 + 3, `T${tier}: 2 + 1 summons`);
|
||||
assert.ok(u.skill.active, `T${tier}: one link left`);
|
||||
h.b.kill(slept[1], null);
|
||||
h.step(3);
|
||||
assert.equal(u.skill.active, false, `T${tier}: every link broken ⇒ ends`);
|
||||
done(h);
|
||||
}
|
||||
// the end releases the sleep; a stun interrupts it
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 1 });
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
assert.ok(e.findBuff('sleep'));
|
||||
h.b.applyStatus(u, 'stun', { duration: 1 });
|
||||
h.step(3);
|
||||
assert.ok(!u.skill.active && !e.findBuff('sleep'), 'stunned: interrupted, the sleep gone');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 冠死以冕: the next 悲叹的仆役 is the special form (+200 % HP / ATK, +100 % DEF, +10 RES, +0.7 s, block +1; 怨火缠身 15 SP / 8 s, 120 % ATK / s on its 1-1); the cast (SKILL_RANGE, 3-3 meanwhile): 2 strikes at her attack interval on every enemy of the 3-3 (550 % / 650 %), each consuming a normal summon to upgrade it (twice for an upgraded one; +80 / 80 / 40 %, heal 20 %, 3rd: block +1, 攻击距离 +1)', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
const v = skelOf(tier, elite, 2);
|
||||
const dr = formOf(tier, elite).talents.find((t) => t.index === 3).bb;
|
||||
assert.deepEqual([dr.max_hp, dr.atk, dr.def, dr.magic_resistance, dr.base_attack_time, dr.block_cnt], [2, 2, 1, 10, 0.7, 1]);
|
||||
assert.deepEqual([sk.bb['attack@atk_scale'], sk.bb['attack@max_hp'], sk.bb['attack@atk'], sk.bb['attack@def'], sk.bb['attack@hp_ratio'], sk.bb['attack@max_stack_cnt'], sk.rangeId], [elite ? 6.5 : 5.5, 0.8, 0.8, 0.4, 0.2, 6, '3-3'], `T${tier}`);
|
||||
assert.deepEqual([v.skill.skillId, v.skill.spCost, v.skill.bb['attack@atk_scale'], v.skill.bb['attack@duration']], ['sktok_necras_skeltn_3', 15, 1.2, 8]);
|
||||
const { h, u } = field({ tier, elite, skill: 2 });
|
||||
killAt(h, [10, 5]);
|
||||
h.run(0.2);
|
||||
const [d] = skels(u);
|
||||
assert.ok(d.mem.necrasDragon, `T${tier}: the first one is the special form`);
|
||||
assert.deepEqual([d.s.maxHp, d.s.atk, d.s.def, d.s.res, d.s.blockCnt], [d.base.maxHp * 3, d.base.atk * 3, d.base.def * 2, d.base.res + 10, 2], `T${tier}: the special form's attributes`);
|
||||
approx(d.s.bat, d.base.bat + 0.7, `T${tier}: +0.7 s`);
|
||||
assert.ok(!d.s.flags.noSp && d.skill.spCost === 15, `T${tier}: its skill charges`);
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
killAt(h, [11, 4]);
|
||||
h.run(0.2);
|
||||
const [, n1, n2] = skels(u);
|
||||
assert.ok(!n1.mem.necrasDragon && !n2.mem.necrasDragon && n1.s.flags.noSp, `T${tier}: the others normal, 阻回`);
|
||||
// upgrade n1 (the earliest normal one), so its consumption upgrades twice
|
||||
killAt(h, [10, 4]);
|
||||
h.run(0.2);
|
||||
assert.equal([n1, n2].filter((x) => x.findBuff('necras:upgrade')).length, 1, `T${tier}: one normal summon upgraded`);
|
||||
const up1 = n1.findBuff('necras:upgrade') ? n1 : n2;
|
||||
// the cast
|
||||
const foe = h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
const u0atk = u.s.atk;
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 2), `T${tier}: SKILL_RANGE — an enemy in the 3-3`);
|
||||
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-3 while it runs`);
|
||||
const tCast = h.b.time;
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 5), `T${tier}: ends after its 2 strikes`);
|
||||
const strikes = h.hooksOf('damaged').filter((x) => x.source === u && x.target === foe && x.dmg.tags?.includes('necras:s3'));
|
||||
assert.equal(strikes.length, 2, `T${tier}: 2 strikes`);
|
||||
approx(strikes[0].amount, u0atk * sk.bb['attack@atk_scale'] * (1 - foe.s.res / 100), `T${tier}: ${sk.bb['attack@atk_scale'] * 100} % arts`, 1e-3);
|
||||
approx(strikes[1].t - strikes[0].t, u.s.interval, `T${tier}: at her attack interval`, 0.05);
|
||||
void tCast;
|
||||
h.run(0.3);
|
||||
assert.ok(!n1.alive && !n2.alive, `T${tier}: both normal summons consumed`);
|
||||
void up1;
|
||||
assert.equal(d.mem.necrasUps, 3, `T${tier}: 2 (the upgraded one) + 1 upgrades`);
|
||||
assert.deepEqual(d.findBuff('necras:dragonUp').mods, { hpPct: 0.8 * 3, atkPct: 0.8 * 3, defPct: 0.4 * 3, blockCnt: 1, rangeExtend: 1 }, `T${tier}: 3 upgrades — block +1, 攻击距离 +1`);
|
||||
assert.equal(u.liveRangeGrid.length, formOf(tier, elite).rangeGrid.length, `T${tier}: her range back`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('怨火缠身 (the special form\'s skill): 15 SP over time, 8 s, its ATK × 120 % arts every second on every enemy of its 1-1; the normal forms never cast (阻回)', () => {
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2 });
|
||||
killAt(h, [10, 5]);
|
||||
h.run(0.2);
|
||||
const [d] = skels(u);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
assert.ok(h.runUntil(() => d.skill.active, 17), 'cast at 15 SP with an enemy in range');
|
||||
approx(d.skill.timeLeft, 8, '8 s', 0.05);
|
||||
const t0 = h.b.time;
|
||||
h.runUntil(() => !d.skill.active, 10);
|
||||
const ticks = h.hooksOf('damaged').filter((x) => x.source === d && x.target === e && x.dmg.isSkill && x.t >= t0 - 1e-9);
|
||||
assert.equal(ticks.length, 8, 'one per second for 8 s');
|
||||
approx(ticks[0].amount, d.s.atk * 1.2 * (1 - e.s.res / 100), '120 % arts', 1e-3);
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('复燃: a stealthed enemy knocked out on her range counts (天赋无视隐匿); with no free tile left the summon becomes the upgrade', () => {
|
||||
const tiles = [[9, 3], [9, 4], [9, 5], [11, 3], [11, 4], [11, 5], [10, 4], [10, 6]];
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 1, others: tiles.map(([r, c], i) => ({ uid: 3 + i, chessId: 'chess_char_1_02_a', row: r, col: c })) });
|
||||
const e = h.spawn('enemy_weak', { pos: [10, 5] });
|
||||
h.step();
|
||||
h.b.addBuff(e, { key: 'test:stealth', flags: { stealth: true } });
|
||||
h.step();
|
||||
h.b.kill(e, null);
|
||||
h.run(0.2);
|
||||
const [s1] = skels(u);
|
||||
assert.ok(s1, 'a summon for the stealthed one');
|
||||
assert.deepEqual([s1.tileR, s1.tileC], [10, 5]);
|
||||
killAt(h, [9, 4]);
|
||||
h.run(0.2);
|
||||
assert.equal(skels(u).length, 1, 'no free tile: no second summon');
|
||||
assert.ok(s1.findBuff('necras:upgrade'), '… the upgrade instead');
|
||||
done(h);
|
||||
});
|
||||
+367
-96
@@ -4495,8 +4495,8 @@
|
||||
"snaps": "9e71a576 5f85bc01 c3ec0c6e 907b650d 8d0a1507 16eb2156 378d038b 6506577e 30807fb9 f2c86b9d ec752e72 d7303d83 83002a0a 01b70ca0 09e839c4 98d2358c 161de0dc 5f1fb139 3915cceb aeddf47d a8963bbe 76037450 84a29f92"
|
||||
},
|
||||
"diy-106": {
|
||||
"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=ironmn/S1 5_diy2_a=ironmn/S2 5_diy1_a=ironmn/S3 5_diy2_b=ironmn/S1 5_diy1_b=ironmn/S2 5_diy2_b=ironmn/S3 5_diy1_b=ironmn/S1/002_ironmn 5_diy2_b=ironmn/S2/002_ironmn 5_diy1_b=ironmn/S3/002_ironmn 5_diy2_b=ironmn/S1/003_ironmn 5_diy1_b=ironmn/S2/003_ironmn 5_diy2_b=ironmn/S3/003_ironmn",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m07",
|
||||
"modeId": "mode_multi_abyss",
|
||||
@@ -4512,111 +4512,111 @@
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":126,"perfect":false,"leaks":126,"leaksUncounted":0,"coins":0,"dmg":59922,"bossDmg":0,"heal":8548,"deaths":25,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":0,"total":126,"perfect":false,"leaks":126,"leaksUncounted":0,"coins":0,"dmg":44928,"bossDmg":0,"heal":0,"deaths":22,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,3,0,1808,2128,0,8,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,6,0,9112,3634,0,34,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,83,0,1003,6580,0,8,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,86,0,14738,7835,0,17,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,153,0,9754,4473,0,27,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,94,0,11704,3746,0,27,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,90,0,2802,2266,0,12,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,79,0,3020,2720,0,11,0],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,141,0,0,5763,2109,6,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,128,0,0,3807,6439,10,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,112,0,2354,1864,0,12,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,148,0,3628,4041,0,16,1],
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,150,0,2284,6126,0,8,0],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,108,0,3119,4084,0,8,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,126,0,3536,4084,0,8,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,87,0,3138,4986,0,9,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,140,0,6108,7479,0,12,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,77,0,2092,4986,0,6,0],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_ironmn",0,0,71,0,1185,4986,0,6,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_ironmn",0,0,3,0,3453,2493,0,5,1],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_ironmn",0,0,5,0,7676,2493,0,10,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_ironmn",0,0,7,0,5637,2753,0,8,1],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_ironmn",0,0,9,0,2545,2753,0,5,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_ironmn",0,0,81,0,4156,5506,0,11,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_1251_lysyta_2":[54,0,54,8019,34461],"enemy_1252_lysytb_2":[42,0,42,35352,24551],"enemy_1329_cbshld_2":[30,0,30,5487,911]},
|
||||
"hooks": [1,153,5814,380,380,358,358,16,25,151,5,5,0,8987,28,10,126,126,1,25,0,0,0],
|
||||
"events": {"atk":380,"deploy":25,"die":25,"dmg":358,"heal":16,"leak":126,"skill":10,"spawn":140,"status":50},
|
||||
"enemies": {"enemy_1251_lysyta_2":[54,0,54,16902,36707],"enemy_1252_lysytb_2":[42,0,42,17404,7700],"enemy_1329_cbshld_2":[30,0,30,18422,521]},
|
||||
"hooks": [1,150,5814,251,251,249,249,0,22,148,3,3,0,8526,0,43,126,126,1,22,0,0,0],
|
||||
"events": {"atk":253,"deploy":22,"die":22,"dmg":249,"leak":126,"skill":6,"spawn":140},
|
||||
"fx": {},
|
||||
"snaps": "57186c39 a89f274b e649d7d9 75621917 006f6ba9 7f3b8dda bb3864de e8193348 16cc7f17 db8194aa e07167eb 0946e952 5d27c316 103f7135 f7e98be4 ef855c6a d34a4bb6 5d39faae c6e66696 77bde8df"
|
||||
"snaps": "3a9f9447 79572e55 5c0d9af5 3c790371 c9cc0b63 80cd9e44 2b74cf88 10724b1e 93648206 da37a7be 1d80cfc3 4b885726 fdfe1174 ef7324bf bc908605 f495a489 678b216a 0c01cc94 6e99ea23 6b03c96b"
|
||||
},
|
||||
"diy-107": {
|
||||
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
|
||||
"about": "6_diy1_a=ironmn/S1 6_diy2_a=ironmn/S2 6_diy1_a=ironmn/S3 6_diy2_b=ironmn/S1 6_diy1_b=ironmn/S2 6_diy2_b=ironmn/S3 6_diy1_b=ironmn/S1/002_ironmn 6_diy2_b=ironmn/S2/002_ironmn 6_diy1_b=ironmn/S3/002_ironmn 6_diy2_b=ironmn/S1/003_ironmn 6_diy1_b=ironmn/S2/003_ironmn 6_diy2_b=ironmn/S3/003_ironmn",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m01",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 12,
|
||||
"round": 13,
|
||||
"seed": 4020340196,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 203.3,
|
||||
"ticks": 6099,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 223.97,
|
||||
"ticks": 6719,
|
||||
"reason": "cleared",
|
||||
"killed": 90,
|
||||
"total": 90,
|
||||
"killed": 5,
|
||||
"total": 96,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 450,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":90,"total":90,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":792864,"bossDmg":0,"heal":34212,"deaths":7,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":5,"total":96,"perfect":false,"leaks":91,"leaksUncounted":0,"coins":0,"dmg":65704,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,2604,1,106,0,2245,2604,0,9,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",2367.8,1,105,0,4281,4374,0,19,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,100,5,45221,3058,0,15,4],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,7,3,48414,1904,0,24,6],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,95,13,46166,11997,0,51,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",3411,1,8,3,33047,3326,0,19,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1272,1,2,0,13858,0,0,19,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,3,38,146945,0,0,177,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1372,1,4,1,19328,0,0,20,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1495.56,1,12,27,433358,24418,0,89,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1224,1,1,0,0,0,11831,36,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,5,0,0,0,22381,66,2]
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,2042,1,120,0,4377,4084,0,13,1],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,119,0,4720,6126,0,20,0],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,71,1,3143,4084,0,12,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,99,1,6842,4986,0,14,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,69,0,5463,4986,0,17,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,106,0,5775,4986,0,14,1],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_ironmn",0,0,67,0,5382,4986,0,14,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_ironmn",0,0,113,1,8070,4986,0,13,1],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_ironmn",0,0,2,1,6199,2493,0,11,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_ironmn",0,0,6,1,4672,2893,0,7,1],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_ironmn",0,0,65,0,5022,5786,0,18,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_ironmn",0,0,61,0,6041,5786,0,19,0]
|
||||
],
|
||||
"enemies": {"enemy_1229_darmy":[27,27,0,3192,108864],"enemy_1229_darmy_2":[51,51,0,12310,269280],"enemy_9007_acelem":[12,12,0,36180,414720]},
|
||||
"hooks": [1,106,6099,594,594,1735,1777,160,97,97,28,27,0,32480,0,62,90,0,1,97,0,2,0],
|
||||
"events": {"atk":764,"deploy":16,"die":97,"dmg":1777,"fx":154,"heal":142,"skill":55,"spawn":102,"status":62},
|
||||
"fx": {"aoe":52,"beam":25,"burst":2,"dp":18,"slash":57},
|
||||
"snaps": "03984647 ace8dbe4 49520875 0ad8fbe6 ba7c56bb e58d0120 a539ef32 0e2bc9be 3b333b61 db04449a dace616b 63a39256 5c34a56e d1619dfd 63bd653a d74deb99 1d733ab1 6e8cbc08 04eb4bb7 74b3c8bb 80dc47fc"
|
||||
"enemies": {"enemy_1229_darmy":[27,2,25,12215,18973],"enemy_1229_darmy_2":[57,3,54,35687,44645],"enemy_9007_acelem":[12,0,12,8279,2087]},
|
||||
"hooks": [1,120,6719,345,345,404,558,0,28,119,8,8,0,13945,0,51,96,91,1,28,0,2,0],
|
||||
"events": {"atk":349,"deploy":24,"die":28,"dmg":558,"fx":9,"leak":91,"skill":16,"spawn":108,"status":18},
|
||||
"fx": {"beam":7,"burst":2},
|
||||
"snaps": "313d2a61 8128fd71 a90f20a0 dbb4f993 cd0e7f87 22382d59 9c4c90be e16faec1 403431b9 2e79a446 8393ac30 80940b48 8e20f125 eb123769 016fa3a1 42d6e291 c380f813 fdceb4c7 d5f6f009 e1ce6b90 cbb63499 8d7362dc 2d3dc7e0"
|
||||
},
|
||||
"diy-108": {
|
||||
"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=nasti/S1 5_diy2_a=nasti/S2 5_diy1_a=nasti/S3 5_diy2_b=nasti/S1 5_diy1_b=nasti/S2 5_diy2_b=nasti/S3 5_diy1_b=nasti/S1/002_nasti 5_diy2_b=nasti/S2/002_nasti 5_diy1_b=nasti/S3/002_nasti 6_diy1_a=nasti/S1 6_diy2_a=nasti/S2 6_diy1_a=nasti/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m02",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"round": 12,
|
||||
"seed": 1366160473,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 15,
|
||||
"total": 36,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 219.47,
|
||||
"ticks": 6584,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 72,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 95,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":15,"total":36,"perfect":false,"leaks":21,"leaksUncounted":0,"coins":0,"dmg":223366,"bossDmg":0,"heal":0,"deaths":32,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":0,"total":72,"perfect":false,"leaks":72,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":0,"deaths":22,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,3,0,0,2604,0,0,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,4,0,0,3431,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,5,1,41288,1865,0,28,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,8,2,117437,2374,0,51,1],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1051.96,1,71,0,9905,3535,0,0,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,68,0,22211,5712,0,0,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,72,0,0,10386,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,73,0,0,10136,0,0,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,43,0,0,5276,0,0,0],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,52,0,0,7062,0,0,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,6,0,13315,1160,0,28,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,54,0,19210,2860,0,23,1],
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,1888.22,1,97,0,0,3926,0,0,0],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1938,1,98,0,0,3876,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,92,0,0,3876,0,0,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,96,0,0,4734,0,0,0],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,63,0,0,4734,0,0,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,59,0,0,4734,0,0,0],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_nasti",0,0,56,0,0,4890,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_nasti",0,0,8,0,0,2445,0,0,0],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_nasti",0,0,60,0,0,4890,0,0,0],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,66,0,0,3876,0,0,0],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,64,0,0,3876,0,0,0],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,6,0,0,1938,0,0,0],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[12,12,0,102,639],"enemy_1005_yokai_3":[12,0,11,29968,87430],"enemy_9009_acfort":[12,3,9,26331,135297]},
|
||||
"hooks": [1,73,6900,439,439,497,550,0,47,67,8,24,0,10810,814,0,36,20,1,35,0,0,17],
|
||||
"events": {"atk":472,"deploy":35,"die":47,"dmg":550,"fx":92,"leak":20,"skill":48,"spawn":50,"status":162},
|
||||
"fx": {"beam":35,"dodge":21,"dp":4,"slash":32},
|
||||
"snaps": "7c7487b3 43a949cf 206930da 90551654 2113b5a2 a7c4855b 59932997 42395489 3f83a15d ffbd721d 646072f3 fc3be7c4 b210ec54 960a9e51 aa1af455 09ccfce3 50e6e3d0 380e639b 9eaf5bef 3b943f89 3933d1fc 3718a2f5 a8ae4307"
|
||||
"enemies": {"enemy_1005_yokai_2":[18,0,18,819,0],"enemy_1041_lazerd_2":[24,0,24,46976,0],"enemy_1042_frostd":[30,0,30,0,0]},
|
||||
"hooks": [1,98,6584,197,197,181,181,0,22,94,0,0,0,7405,0,0,72,72,1,22,0,0,0],
|
||||
"events": {"atk":197,"deploy":24,"die":22,"dmg":181,"fx":25,"leak":72,"spawn":86,"status":46},
|
||||
"fx": {"spGift":25},
|
||||
"snaps": "56640ba8 1c39d9ef 0f24c7b3 0fa0cfa7 45a9ab31 f3d3e707 455f2fc5 ebaf173f d017acd6 ba452c9a 1b7373b8 1b4eba9a 7a84314e d23104cc 21feb09c 4ded970b 7cf42f09 51ad43fa 3f316046 80c09be1 d936b443 611528aa"
|
||||
},
|
||||
"diy-109": {
|
||||
"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=nasti/S1 6_diy2_b=nasti/S2 6_diy1_b=nasti/S3 6_diy2_b=nasti/S1/002_nasti 6_diy1_b=nasti/S2/002_nasti 6_diy2_b=nasti/S3/002_nasti 5_diy1_a=necras/S1 5_diy2_a=necras/S2 5_diy1_a=necras/S3 5_diy2_b=necras/S1 5_diy1_b=necras/S2 5_diy2_b=necras/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m03",
|
||||
@@ -4624,38 +4624,309 @@
|
||||
"round": 13,
|
||||
"seed": 500258608,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 215.33,
|
||||
"ticks": 6460,
|
||||
"reason": "cleared",
|
||||
"killed": 15,
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 53,
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 453,
|
||||
"rngDraws": 35,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":15,"total":123,"perfect":false,"leaks":108,"leaksUncounted":0,"coins":0,"dmg":280261,"bossDmg":0,"heal":30749,"deaths":28,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":53,"total":123,"perfect":false,"leaks":70,"leaksUncounted":0,"coins":0,"dmg":779475,"bossDmg":0,"heal":23859,"deaths":18,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,5,0,1376,2285,0,5,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,7,0,982,2494,0,2,0],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,155,2,36128,9372,0,79,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,156,2,52788,20167,0,62,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,140,4,59650,2964,0,77,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,154,6,91210,11325,0,66,1],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,86,0,3596,3133,0,4,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,83,0,19042,8583,0,5,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,4,0,0,2326,11819,20,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,10,0,0,4670,18930,25,1],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,108,0,5752,2320,0,16,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,8,1,9738,1450,0,16,1],
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"none",813.52,1,183,3,28597,6287,0,32,2],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"none",0,0,143,1,12666,4734,0,28,3],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"none",377.74,1,172,3,35651,6723,0,47,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_nasti",2060.13,1,187,2,22965,5476,0,32,2],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_nasti",1212.06,1,91,3,44631,3812,0,90,5],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_nasti",0,0,163,4,46469,5024,0,54,3],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,1443,1,188,3,29486,1443,0,42,5],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,10,6,69691,1443,0,47,4],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,113,0,33963,2886,0,18,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,103,0,6266,3524,0,12,1],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",239.3,1,4,10,153058,1523,0,76,8],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,8,7,164359,1762,0,65,6],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null],
|
||||
["p1","~1","token","token_10043_necras_skeltn",null,null,0,0,21,0,925,1744,0,4,null],
|
||||
["p1","~2","token","token_10043_necras_skeltn",null,null,0,0,36,1,9234,5557,3834,25,null],
|
||||
["p1","~3","token","token_10043_necras_skeltn",null,null,0,0,37,0,1814,4605,0,3,0],
|
||||
["p1","~4","token","token_10043_necras_skeltn",null,null,0,0,38,0,1387,5232,0,2,0],
|
||||
["p1","~5","token","token_10043_necras_skeltn",null,null,0,0,39,0,1210,1535,0,6,null],
|
||||
["p1","~6","token","token_10043_necras_skeltn",null,null,0,0,40,0,202,1535,0,1,null],
|
||||
["p1","~7","token","token_10043_necras_skeltn",null,null,0,0,41,0,806,1535,0,4,null],
|
||||
["p1","~8","token","token_10043_necras_skeltn",null,null,0,0,45,5,46383,5232,0,18,3],
|
||||
["p1","~9","token","token_10043_necras_skeltn",null,null,0,0,47,0,1169,1535,0,5,null],
|
||||
["p1","~10","token","token_10043_necras_skeltn",null,null,0,0,48,0,3537,3785,297,8,null],
|
||||
["p1","~11","token","token_10043_necras_skeltn",null,null,0,0,50,0,1048,1535,0,4,null],
|
||||
["p1","~12","token","token_10043_necras_skeltn",null,null,155.89,0,53,0,2379,3737,1646,7,null],
|
||||
["p1","~13","token","token_10043_necras_skeltn",null,null,1535,0,54,0,1008,0,0,5,null],
|
||||
["p1","~14","token","token_10043_necras_skeltn",null,null,0,0,55,0,1845,3070,0,5,null],
|
||||
["p1","~15","token","token_10043_necras_skeltn",null,null,0,0,56,0,635,3261,191,2,null],
|
||||
["p1","~16","token","token_10043_necras_skeltn",null,null,2497.95,0,59,0,0,1121,1097,0,null],
|
||||
["p1","~17","token","token_10043_necras_skeltn",null,null,0,0,63,1,5694,6164,2676,15,null],
|
||||
["p1","~18","token","token_10043_necras_skeltn",null,null,0,0,92,0,605,1535,0,3,null],
|
||||
["p1","~19","token","token_10043_necras_skeltn",null,null,0,0,94,0,605,3070,0,2,null],
|
||||
["p1","~20","token","token_10043_necras_skeltn",null,null,0,0,96,0,1935,1535,0,8,null],
|
||||
["p1","~21","token","token_10043_necras_skeltn",null,null,0,0,97,0,806,3070,0,2,null],
|
||||
["p1","~22","token","token_10043_necras_skeltn",null,null,0,0,98,0,1210,3070,0,3,null],
|
||||
["p1","~23","token","token_10043_necras_skeltn",null,null,0,0,100,0,806,1535,0,4,null],
|
||||
["p1","~24","token","token_10043_necras_skeltn",null,null,0,0,108,0,462,1744,0,2,null],
|
||||
["p1","~25","token","token_10043_necras_skeltn",null,null,0,0,110,0,3745,5232,0,5,0],
|
||||
["p1","~26","token","token_10043_necras_skeltn",null,null,0,0,112,0,403,1535,0,2,null],
|
||||
["p1","~27","token","token_10043_necras_skeltn",null,null,0,0,116,0,647,1744,0,2,null],
|
||||
["p1","~28","token","token_10043_necras_skeltn",null,null,3070,0,118,0,0,0,0,0,null],
|
||||
["p1","~29","token","token_10043_necras_skeltn",null,null,3070,0,119,0,0,0,0,0,null],
|
||||
["p1","~30","token","token_10043_necras_skeltn",null,null,2879.32,0,121,0,0,465,549,0,null],
|
||||
["p1","~31","token","token_10043_necras_skeltn",null,null,712.15,0,122,0,0,823,0,0,null],
|
||||
["p1","~32","token","token_10043_necras_skeltn",null,null,2876.74,0,125,0,2081,2355,0,3,0],
|
||||
["p1","~33","token","token_10043_necras_skeltn",null,null,0,0,132,0,1699,1744,0,9,null],
|
||||
["p1","~34","token","token_10043_necras_skeltn",null,null,0,0,133,0,8485,5608,2120,18,null],
|
||||
["p1","~35","token","token_10043_necras_skeltn",null,null,712.15,0,151,0,1210,823,0,6,null],
|
||||
["p1","~36","token","token_10043_necras_skeltn",null,null,0,0,162,0,1156,3488,0,3,null],
|
||||
["p1","~37","token","token_10043_necras_skeltn",null,null,3488,1,170,2,15252,5331,6797,31,null],
|
||||
["p1","~38","token","token_10043_necras_skeltn",null,null,3488,1,175,2,11289,4652,4652,27,null]
|
||||
],
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,34,21,68313,289208],"enemy_1299_ymkilr_2":[27,2,25,61482,125997],"enemy_1422_lrsldr_2":[39,17,22,15384,364270]},
|
||||
"hooks": [1,188,6900,1796,1796,2240,2240,51,98,175,47,44,0,27488,22,106,123,68,1,98,0,0,0],
|
||||
"events": {"atk":1811,"deploy":62,"die":107,"dmg":1991,"fx":398,"heal":26,"leak":68,"skill":91,"spawn":176,"status":38},
|
||||
"fx": {"aoe":54,"grow":51,"shield":55,"sleep":22,"spGift":178,"summon":38},
|
||||
"snaps": "94942597 3f389ccc 71e59db9 99137ad5 10526115 3f9add15 dbabcaf1 5edc4dac 7b40b75d 2cf88d9c c681c609 228ae7fe d8dd326b e9e118c9 864cf266 5138d0c5 fe8ac5f4 dfd62790 8c473664 0ba217de 13127480 e7a5c977 31b40ca3"
|
||||
},
|
||||
"diy-110": {
|
||||
"about": "5_diy1_b=necras/S1/002_necras 5_diy2_b=necras/S2/002_necras 5_diy1_b=necras/S3/002_necras 6_diy1_a=necras/S1 6_diy2_a=necras/S2 6_diy1_a=necras/S3 6_diy2_b=necras/S1 6_diy1_b=necras/S2 6_diy2_b=necras/S3 6_diy1_b=necras/S1/002_necras 6_diy2_b=necras/S2/002_necras 6_diy1_b=necras/S3/002_necras",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m04",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 13,
|
||||
"seed": 2140781790,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 223.03,
|
||||
"ticks": 6691,
|
||||
"reason": "cleared",
|
||||
"killed": 9,
|
||||
"total": 144,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 19,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":9,"total":144,"perfect":false,"leaks":135,"leaksUncounted":0,"coins":0,"dmg":347681,"bossDmg":0,"heal":5866,"deaths":22,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_necras",1882,1,190,0,56617,3764,0,22,3],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_necras",0,0,169,3,24339,3764,0,20,2],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_necras",0,0,189,0,12902,5646,0,19,1],
|
||||
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,181,0,2336,4329,0,9,0],
|
||||
["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,5,0,33495,1443,0,17,2],
|
||||
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,8,2,67305,1443,0,23,2],
|
||||
["p1",7,"op","chess_char_6_diy2_b",0,"none",0,0,117,0,6990,3524,0,17,2],
|
||||
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,105,0,1893,3524,0,7,0],
|
||||
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,127,0,59085,3524,0,24,2],
|
||||
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_necras",0,0,6,1,21437,1962,0,18,2],
|
||||
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_necras",0,0,7,2,28281,1962,0,14,1],
|
||||
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_necras",0,0,116,0,12799,3924,0,18,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["p1","~1","token","token_10043_necras_skeltn",null,null,0,0,25,0,723,1744,0,5,null],
|
||||
["p1","~2","token","token_10043_necras_skeltn",null,null,3488,0,33,0,780,0,0,2,null],
|
||||
["p1","~3","token","token_10043_necras_skeltn",null,null,0,0,34,1,3359,3488,0,10,null],
|
||||
["p1","~4","token","token_10043_necras_skeltn",null,null,0,0,35,0,1618,4456,968,4,null],
|
||||
["p1","~5","token","token_10043_necras_skeltn",null,null,0,0,36,0,706,1535,0,2,null],
|
||||
["p1","~6","token","token_10043_necras_skeltn",null,null,0,0,37,0,1089,4605,0,3,0],
|
||||
["p1","~7","token","token_10043_necras_skeltn",null,null,100.97,0,38,0,4133,6774,3387,12,null],
|
||||
["p1","~8","token","token_10043_necras_skeltn",null,null,0,0,39,0,121,1535,0,1,0],
|
||||
["p1","~9","token","token_10043_necras_skeltn",null,null,0,0,44,0,882,4088,1511,4,0],
|
||||
["p1","~10","token","token_10043_necras_skeltn",null,null,0,0,48,0,756,4605,0,2,0],
|
||||
["p1","~11","token","token_10043_necras_skeltn",null,null,0,0,53,0,809,3488,0,2,null],
|
||||
["p1","~12","token","token_10043_necras_skeltn",null,null,0,0,55,0,1214,1744,0,3,null],
|
||||
["p1","~13","token","token_10043_necras_skeltn",null,null,0,0,61,0,809,1744,0,2,null],
|
||||
["p1","~14","token","token_10043_necras_skeltn",null,null,0,0,62,0,353,1535,0,1,null],
|
||||
["p1","~15","token","token_10043_necras_skeltn",null,null,0,0,63,0,445,1744,0,2,null],
|
||||
["p1","~16","token","token_10043_necras_skeltn",null,null,0,0,64,0,202,1744,0,1,null],
|
||||
["p1","~17","token","token_10043_necras_skeltn",null,null,0,0,66,0,809,1744,0,2,null],
|
||||
["p1","~18","token","token_10043_necras_skeltn",null,null,0,0,67,0,353,1535,0,1,null],
|
||||
["p1","~19","token","token_10043_necras_skeltn",null,null,0,0,69,0,607,1744,0,3,null],
|
||||
["p1","~20","token","token_10043_necras_skeltn",null,null,0,0,70,0,289,1744,0,2,null],
|
||||
["p1","~21","token","token_10043_necras_skeltn",null,null,0,0,73,0,145,1744,0,1,null]
|
||||
],
|
||||
"enemies": {"enemy_1251_lysyta_2":[51,9,42,6741,117116],"enemy_1252_lysytb_2":[54,0,54,60636,113654],"enemy_1329_cbshld_2":[39,0,39,24772,151911]},
|
||||
"hooks": [1,190,6691,441,441,1548,1548,25,50,187,18,18,0,12477,9,35,144,135,1,50,0,0,0],
|
||||
"events": {"atk":441,"deploy":44,"die":52,"dmg":1548,"fx":76,"heal":5,"leak":135,"skill":36,"spawn":179,"status":14},
|
||||
"fx": {"aoe":21,"grow":25,"sleep":9,"summon":21},
|
||||
"snaps": "8fcc5e4f 0f7da186 17d196b7 b58c651e 4c9a47bd 86f69a05 8c551995 be136342 d6497f7b 32baaaff da84bd33 af2653ed b9f3ea94 7788fcaa 14724b5e 07d44336 39c114a6 dc76a54d c41d4fc4 b6f2c2eb 438f2af0 ebf05254 15fe2e83"
|
||||
},
|
||||
"diy-111": {
|
||||
"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": "act1autochess_m01",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 12,
|
||||
"seed": 452407399,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 216.83,
|
||||
"ticks": 6505,
|
||||
"reason": "cleared",
|
||||
"killed": 13,
|
||||
"total": 99,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":13,"total":99,"perfect":false,"leaks":86,"leaksUncounted":0,"coins":0,"dmg":304937,"bossDmg":0,"heal":8682,"deaths":15,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,123,0,1472,6384,0,32,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,81,1,11090,5816,0,90,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,120,1,19609,7692,0,33,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",3334,1,124,4,33655,6668,0,37,1],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1271,1,4,4,51924,0,0,185,3],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,5,1,71779,0,0,187,3],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,11,1,41478,0,0,153,3],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,13,0,49245,0,0,159,3],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,6,0,0,0,8682,26,3],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,14,0,0,0,0,0,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,114,0,3950,2796,0,17,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,86,1,20735,10944,0,78,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,10,47,52058,82202],"enemy_1299_ymkilr_2":[27,0,27,9819,9240],"enemy_1422_lrsldr_2":[39,5,34,9213,188819]},
|
||||
"hooks": [1,156,6460,632,632,849,850,92,43,151,7,20,0,9133,611,30,123,108,1,43,0,0,0],
|
||||
"events": {"atk":705,"deploy":30,"die":43,"dmg":822,"fx":36,"heal":76,"leak":108,"skill":42,"spawn":138,"status":149},
|
||||
"fx": {"aoe":2,"displace":17,"dodge":2,"spGain":15},
|
||||
"snaps": "17351197 d85b764f 170d4969 276e2353 d5273c76 84c8775e 178ead82 e788140a 7761c24b 8d4c0668 9647f92d 7caa4901 24e1ffd3 00675a6a 82e3afd4 846d95f4 87dfd927 4f075077 94a7b0a4 a3a639d3 4179fb65 3ad0ed6c"
|
||||
"enemies": {"enemy_1010_demon":[42,2,40,21226,115683],"enemy_1320_wdrrl_2":[12,0,12,5886,12532],"enemy_1422_lrsldr_2":[45,11,34,13189,176722]},
|
||||
"hooks": [1,124,6505,1247,1247,1221,1221,26,28,114,20,19,0,28400,94,34,99,86,1,28,0,0,0],
|
||||
"events": {"atk":1247,"deploy":22,"die":28,"dmg":1221,"fx":1,"heal":26,"leak":86,"skill":39,"spawn":114,"status":187},
|
||||
"fx": {"explode":1},
|
||||
"snaps": "c483ab5c c43d24d1 3012b5e1 e015a05e 6b17b1fe 40152fac 120f3052 92460f9e 6d430249 0820bac0 f6e91bf2 f81209a2 fa43731e 7086293d cd16e934 56c0a32c e19ec163 5a5ef609 334df640 1a69c7ed 52f1c2f5 da5425f6"
|
||||
},
|
||||
"diy-112": {
|
||||
"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_m02",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 12,
|
||||
"seed": 3106580722,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 197.6,
|
||||
"ticks": 5928,
|
||||
"reason": "cleared",
|
||||
"killed": 9,
|
||||
"total": 135,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 204,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":9,"total":135,"perfect":false,"leaks":126,"leaksUncounted":0,"coins":0,"dmg":415122,"bossDmg":0,"heal":16340,"deaths":22,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,3,1,3246,2604,0,25,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,1,1,14125,3371,0,45,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,124,2,41582,6571,0,29,3],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,104,3,101035,6650,0,27,3],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,103,0,21884,5680,0,19,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,101,2,65446,11148,0,33,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,111,0,20912,3294,0,22,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",119.44,1,158,0,25637,5375,0,39,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,6,0,68224,2164,0,14,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,95,0,53031,4607,0,13,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,141,0,0,4932,1260,3,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,153,0,0,3438,15080,14,1]
|
||||
],
|
||||
"enemies": {"enemy_2021_syfish":[105,9,96,13256,339526],"enemy_2025_syufo":[30,0,30,46578,75596]},
|
||||
"hooks": [1,158,5928,586,586,945,1056,33,31,157,15,15,0,13917,0,30,135,126,1,59,0,16,29],
|
||||
"events": {"atk":632,"deploy":23,"die":31,"dmg":1056,"fx":153,"heal":32,"leak":126,"skill":30,"spawn":147,"status":66},
|
||||
"fx": {"aoe":42,"burst":16,"dodge":29,"dp":11,"slash":48,"spGain":7},
|
||||
"snaps": "f1fffa0d 2e4ead28 c68dbfb0 75d60e92 28dd7c37 394efa53 2f2aabd0 2343aed0 5ead163f 3efc95b3 7649823f e5961d99 7f86e2ed 8c75f677 2b7c65c1 de7b4948 22db0221 4ff706e7 f02a4505 f70972db"
|
||||
},
|
||||
"diy-113": {
|
||||
"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_m03",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 13,
|
||||
"seed": 1466195980,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 200.47,
|
||||
"ticks": 6014,
|
||||
"reason": "cleared",
|
||||
"killed": 69,
|
||||
"total": 144,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 203,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":69,"total":144,"perfect":false,"leaks":75,"leaksUncounted":0,"coins":0,"dmg":508542,"bossDmg":0,"heal":0,"deaths":24,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,4,4,17615,2604,0,49,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,5,6,24416,3431,0,48,1],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,160,6,53183,3730,0,70,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,155,15,157188,4748,0,87,1],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,41,1,172,5,40925,4546,0,21,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1805,1,175,16,100705,3907,0,33,4],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1121,1,176,1,25672,9265,0,50,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1185,1,121,4,15855,1711,0,34,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,2041,1,166,4,18403,3235,0,22,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",1250,1,143,6,17122,5812,0,14,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,178,1,17171,1160,0,53,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,135,1,20287,2860,0,42,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_2021_syfish":[99,58,41,7589,243386],"enemy_2025_syufo":[45,11,34,39420,265157]},
|
||||
"hooks": [1,178,6014,1142,1142,1613,2045,0,93,168,14,23,0,18585,1230,71,144,75,1,93,0,32,16],
|
||||
"events": {"atk":1184,"deploy":31,"die":93,"dmg":2031,"fx":189,"leak":75,"skill":49,"spawn":159,"status":356},
|
||||
"fx": {"aoe":60,"burst":32,"displace":5,"dodge":22,"dp":27,"slash":43},
|
||||
"snaps": "de90f0a5 759b0c3a c39a5b8a fa9acb47 839ff747 a9da663c 92684f45 89723c39 48fd5534 c8914ffd f805490d c236abdb f8e576f2 c08df0a5 c194716c e8ee08eb 096b0ab7 b3b92f7b b13fcc00 9be81275"
|
||||
},
|
||||
"diy-114": {
|
||||
"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_m04",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"seed": 4120508441,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 205.67,
|
||||
"ticks": 6170,
|
||||
"reason": "cleared",
|
||||
"killed": 122,
|
||||
"total": 192,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 991,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":122,"total":192,"perfect":false,"leaks":70,"leaksUncounted":0,"coins":0,"dmg":670162,"bossDmg":0,"heal":46494,"deaths":25,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,178,5,43295,3934,0,51,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,6,22,13148,2494,0,39,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,219,2,21846,8078,0,19,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,224,5,52253,13200,0,51,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,4,23,79207,0,0,180,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,5,35,114330,0,0,186,2],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,1372,1,15,4,216108,0,0,75,2],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,205,9,72144,3638,0,33,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,190,0,0,2908,11119,25,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,12,0,0,24392,35374,57,2],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,139,1,13963,4532,0,46,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,14,2,43869,19327,0,91,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1197_sfshu": [15,11,4,1177,60295],
|
||||
"enemy_1197_sfshu_2": [39,22,17,11441,182028],
|
||||
"enemy_1198_msfshu": [33,28,5,0,87],
|
||||
"enemy_1198_msfshu_2": [66,51,15,0,216],
|
||||
"enemy_9010_acpupp": [39,10,29,60856,505701]
|
||||
},
|
||||
"hooks": [1,224,6170,969,969,3329,3121,155,147,217,15,29,0,21470,5920,45,192,70,1,161,0,0,0],
|
||||
"events": {"atk":1309,"deploy":29,"die":147,"dmg":3064,"fx":156,"heal":114,"leak":70,"skill":56,"spawn":207,"status":688},
|
||||
"fx": {"aoe":18,"displace":24,"dodge":5,"ember":14,"revive":2,"shieldBreak":41,"spGain":5,"summon":33,"telegraph":14},
|
||||
"snaps": "052f6301 53938868 38f85ad5 39e3b0ee 87b525a1 113be68f a004c4d3 f24bde7d 54cd2c7c 0ed4e64a f8ceb24d 42d7ad5a e71b7cd4 6db6b3d9 6594d286 6a111077 2d0b44a1 366e5b47 86d5661e 095a92d8 3492d771"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// test/sim/feedback5-o23-engine.test.js — the additive engine rule the 0.2.0 自选 kit of 白铁 needs: ally targets
|
||||
// (Battle.setAllyTarget / allyTargetsInKeys / isAllyTarget, ai.js acquireTargets / performAttack). 白铁's 铁钳号·原型机 is a
|
||||
// summon of the enemy camp (PRTS 铁钳号·原型机 备注 "该召唤物阵营为敌方") that "可被我方干员攻击但不受伤害": our attacks select a
|
||||
// registered ally standing on the attacker's range after every enemy (嘲讽等级 −2), ranged attacks fly to it; heals never
|
||||
// select it; with none registered the attack loop is exactly the old one. Cancelling the damage it takes is the kit's job
|
||||
// (test/content/op_ironmn.test.js); here a plain guard is registered and takes the hits.
|
||||
// Run: node --test test/sim/feedback5-o23-engine.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { acquireTargets } from '../../server/sim/ai.js';
|
||||
|
||||
const ENEMIES = { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e9, speed: 0, mass: 0 }) };
|
||||
const CHESS = {
|
||||
test_guard_a: chessRec({ id: 'test_guard_a', profession: 'WARRIOR', stats: { maxHp: 1e7, atk: 300, def: 0, res: 0 } }),
|
||||
test_sniper_a: chessRec({ id: 'test_sniper_a', profession: 'SNIPER', stats: { maxHp: 2000, atk: 200, def: 0, res: 0 }, rangeGrid: [[0, 0], [0, 1], [0, 2], [0, 3]] }),
|
||||
test_medic_a: chessRec({ id: 'test_medic_a', profession: 'MEDIC', subProf: 'physician', stats: { maxHp: 2000, atk: 100, def: 0, res: 0 }, rangeGrid: [[0, 0], [0, 1], [0, 2]] }),
|
||||
};
|
||||
|
||||
function field() {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES, chess: CHESS }, timeLimit: 600, autoFinish: false, seed: 3, hooks: ['damaged'], captureNoisy: true,
|
||||
units: [
|
||||
{ uid: 1, chessId: 'test_guard_a', row: 10, col: 4 }, // melee 1-1: (10, 4), (10, 5)
|
||||
{ uid: 2, chessId: 'test_guard_a', row: 10, col: 5 }, // the ally target
|
||||
{ uid: 3, chessId: 'test_sniper_a', row: 10, col: 2 }, // ranged: (10, 2) … (10, 5)
|
||||
{ uid: 4, chessId: 'test_medic_a', row: 10, col: 3 },
|
||||
],
|
||||
});
|
||||
h.step();
|
||||
return { h, guard: h.unit(1), target: h.unit(2), sniper: h.unit(3), medic: h.unit(4) };
|
||||
}
|
||||
|
||||
test('ally targets: none registered ⇒ nobody selects an ally; registered ⇒ the attackers whose range holds it select it, after every enemy', () => {
|
||||
const { h, guard, target, sniper, medic } = field();
|
||||
assert.equal(h.b.isAllyTarget(target), false);
|
||||
assert.deepEqual(acquireTargets(h.b, guard, guard.profile), [], 'not registered: no target');
|
||||
assert.ok(h.b.setAllyTarget(target, true));
|
||||
assert.ok(h.b.isAllyTarget(target));
|
||||
assert.deepEqual(h.b.allyTargetsInKeys(guard.rangeKeys, guard).map((u) => u.id), [target.id]);
|
||||
assert.deepEqual(h.b.allyTargetsInKeys(target.rangeKeys, target), [], 'never itself');
|
||||
assert.deepEqual(acquireTargets(h.b, guard, guard.profile).map((u) => u.id), [target.id], 'no enemy: the ally target');
|
||||
assert.deepEqual(acquireTargets(h.b, medic, medic.profile), [], 'a healer never selects it');
|
||||
// an enemy in range comes first; all-in-range / two targets take both
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.step();
|
||||
assert.deepEqual(acquireTargets(h.b, guard, guard.profile).map((u) => u.id), [e.id], 'one target: the enemy');
|
||||
assert.deepEqual(acquireTargets(h.b, guard, { ...guard.profile, maxTargets: 2 }).map((u) => u.id), [e.id, target.id], 'two: the enemy, then it');
|
||||
assert.deepEqual(acquireTargets(h.b, guard, { ...guard.profile, allInRange: true }).map((u) => u.id), [e.id, target.id], 'all in range: both');
|
||||
h.b.setAllyTarget(target, false);
|
||||
assert.deepEqual(acquireTargets(h.b, guard, { ...guard.profile, maxTargets: 2 }).map((u) => u.id), [e.id], 'dropped: enemies only');
|
||||
void sniper;
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('ally targets: attacks on it are real attacks (melee instant, ranged by projectile); off the field it is skipped', () => {
|
||||
const { h, guard, target, sniper } = field();
|
||||
h.b.setAllyTarget(target, true);
|
||||
h.run(3);
|
||||
const by = (src) => h.hooksOf('damaged').filter((c) => c.source === src && c.target === target && c.dmg?.isAttack);
|
||||
assert.ok(by(guard).length >= 1, 'the guard hits it');
|
||||
assert.ok(by(sniper).length >= 1, 'the sniper hits it');
|
||||
let flew = false;
|
||||
for (let i = 0; i < 90 && !flew; i++) { h.step(); flew = h.b.projectiles.list.some((p) => p.source === sniper && p.target === target); }
|
||||
assert.ok(flew, 'by projectile');
|
||||
h.b.retreat(target, { reason: 'retreat' });
|
||||
h.step();
|
||||
assert.deepEqual(h.b.allyTargetsInKeys(guard.rangeKeys, guard), [], 'off the field: skipped while registered');
|
||||
assert.ok(h.b.isAllyTarget(target));
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
Reference in new issue
Block a user