Files
ark-stronghold-protocol/test/content/standin_acfend.test.js
T

206 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test/content/standin_acfend.test.js — the Mechanist (char_610_acfend) 补位 stand-in kit (kits/ops/standin-acfend.js) on
// every chess she replaces: 4 星熊 (S2), 5 号角 / 6 余 (S3, module PRO-X on the elite); S1 through the harness
// `standIn: { skillIndex: 0 }`. Numbers: data/backups.json (skill_table, PRTS Mechanist(卫戍协议) and its 备注: the 0.1 s
// checks of both talents, S2's swap of the talent effect). Her triggers: the owner's 重装 exception for the stand-ins.
// Run: node --test test/content/standin_acfend.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 { unitForm } from '../../shared/standIn.js';
import { TICK } from '../../server/sim/constants.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const CHAR = 'char_610_acfend';
const S1 = 'skchr_acfend_1', S2 = 'skchr_acfend_2';
const RECORDS = BACKUPS.units[CHAR].standsIn.flatMap((a) => [a, a.replace(/_a$/, '_b')]);
const dummy = (o = {}) => enemyRec({ key: 'enemy_dummy', hp: 1e9, speed: 0, ...o });
const DEFS = {
enemies: {
enemy_dummy: dummy(),
enemy_res: dummy({ key: 'enemy_res', res: 20 }),
enemy_fly: dummy({ key: 'enemy_fly', motion: 'FLY' }),
},
};
const close = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const formOf = (id) => unitForm(BACKUPS, CHAR, CHESS[id].status);
const skillOf = (id, skillId) => formOf(id).skills.find((s) => s.skillId === skillId);
const moduleOf = (id) => (CHESS[id].backup.uniEquipId && CHESS[id].status.equipLevel > 0 ? formOf(id).modules.find((m) => m.uniEquipId === CHESS[id].backup.uniEquipId) : null);
function battle(opts) {
const log = [];
const h = makeBattle({
defs: DEFS, timeLimit: 300, autoFinish: false, seed: 5, flags: { dpPerSec: 0 }, ...opts,
setup(b) {
b.on('damaged', (c) => log.push({ t: b.time, src: c.source?.id ?? null, tgt: c.target.id, key: c.target.defId, amount: c.amount, type: c.type, tags: c.dmg?.tags ?? [], skill: !!c.dmg?.isSkill }), { priority: -999 });
if (opts.setup) opts.setup(b);
},
});
h.dmg = log;
return h;
}
/** DEF % of her talent and module at this moment (精研材料 +10 %, its extra below half HP, PRO-X while blocking). */
const passiveDefPct = (id, u) => {
const t0 = u.def.talents[0].bb;
const mod = moduleOf(id);
return t0.def + (u.hpRatio < t0.hp_ratio ? t0['acfend_t_1[extra].def'] : 0) + (mod && u.blocking.length ? mod.traitOverride.bb.def : 0);
};
// ---------------------------------------------------------------------------------------------------------------
test('Mechanist fields on all 6 records she replaces: her body (+ PRO-X attributes on the 5 / 6 elites), the chess\'s backup skill, her own kit, the 重装 trigger', () => {
assert.equal(RECORDS.length, 6);
for (const id of RECORDS) {
const c = CHESS[id];
const form = formOf(id);
const sk = form.skills.find((s) => s.index === c.backup.skillIndex);
const mod = moduleOf(id);
const h = battle({ units: [{ chessId: id, row: 10, col: 4, standIn: true }] });
h.step();
const u = h.unit(1);
assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id], [CHAR, c.charId, sk.skillId], id);
assert.ok(!u.kit.generic && u.kit.skillSource === 'skills', `${id}: her kit's own spec`);
assert.equal(!!u.def.raw.module?.active, !!mod, `${id}: module`);
assert.deepEqual([u.base.def, u.base.maxHp, u.base.blockCnt], [form.stats.def + (mod?.attr.def ?? 0), form.stats.maxHp + (mod?.attr.maxHp ?? 0), 3], `${id}: stats`);
// STANDIN_TRIGGER_DEVIATIONS: DEFAULT (the official TAKE_DAMAGE kept as rawRule)
assert.deepEqual([u.skill.rule, sk.trigger.rawRule], ['DEFAULT', 'TAKE_DAMAGE'], `${id}: trigger`);
assert.equal(u.profile.canHitFly, false, `${id}: ground-only`);
assert.equal(h.b.errors.length, 0);
}
assert.deepEqual([CHESS.chess_char_4_17_a, CHESS.chess_char_5_08_a, CHESS.chess_char_6_03_a].map((c) => c.backup.skillIndex), [1, 2, 2]);
});
test('her skills cast with an enemy she blocks before anything hurts her (the owner\'s 重装 exception) — never on an air unit she cannot reach', () => {
for (const id of ['chess_char_4_17_a', 'chess_char_5_08_b', 'chess_char_6_03_a']) {
const h = battle({ units: [{ chessId: id, row: 10, col: 4, standIn: true, carryState: { sp: 999 } }], enemies: [{ key: 'enemy_fly', pos: [10, 4] }] });
const u = h.unit(1);
h.run(2);
assert.equal(u.skill.activations, 0, `${id}: an air unit on her tile is no target`);
h.spawn('enemy_dummy', { pos: [10, 4] }); // ATK 0: it never damages her
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), `${id}: cast`);
assert.equal(h.dmg.filter((d) => d.tgt === u.id).length, 0, `${id}: untouched`);
checkInvariants(h.b);
}
});
test('精研材料: DEF +10 %; below 50 % HP another +10 % (+20 % with PRO-X Lv3), checked every 0.1 s', () => {
for (const [id, extra] of [['chess_char_4_17_a', 0.1], ['chess_char_5_08_b', 0.1], ['chess_char_6_03_b', 0.2]]) {
const h = battle({ units: [{ chessId: id, row: 10, col: 4, standIn: true }] });
const u = h.unit(1);
h.step();
assert.equal(u.def.talents[0].bb['acfend_t_1[extra].def'], extra, id);
close(u.s.def, u.base.def * 1.1, `${id}: +10 %`);
h.b.loseHp(u, u.hp * 0.55);
h.run(0.1 + TICK);
close(u.s.def, u.base.def * (1.1 + extra), `${id}: below half HP`);
h.b.heal(u, u, u.s.maxHp);
h.run(0.1 + TICK);
close(u.s.def, u.base.def * 1.1, `${id}: healed`);
checkInvariants(h.b);
}
});
test('反馈装甲: enemies she blocks ASPD −7 within 0.1 s; it ends once nobody blocks them — at her retreat too', () => {
const h = battle({ units: [{ chessId: 'chess_char_4_17_a', row: 10, col: 4, standIn: true }], enemies: [{ key: 'enemy_dummy', pos: [10, 4] }, { key: 'enemy_dummy', pos: [10, 4] }, { key: 'enemy_fly', pos: [10, 5] }] });
const u = h.unit(1);
h.run(0.1 + TICK);
const [a, b, f] = h.b.enemies;
assert.deepEqual([a.s.aspd, b.s.aspd, f.s.aspd], [93, 93, 100], 'the two blocked enemies, not the air unit');
assert.equal(a.findBuff('acfend:feedback').source, u);
h.b.retreat(u);
h.run(0.1 + TICK);
assert.deepEqual([a.s.aspd, b.s.aspd], [100, 100], 'released: gone at the next check');
checkInvariants(h.b);
});
test('S1 结构稳定: 40 s of max HP / DEF +26 % (+40 % elite)', () => {
for (const [id, v] of [['chess_char_4_17_a', 0.26], ['chess_char_4_17_b', 0.4]]) {
assert.deepEqual([skillOf(id, S1).bb.max_hp, skillOf(id, S1).bb.def, skillOf(id, S1).duration], [v, v, 40]);
const h = battle({ units: [{ chessId: id, row: 10, col: 4, standIn: { skillIndex: 0 }, carryState: { sp: 999 } }], enemies: [{ key: 'enemy_dummy', pos: [10, 4] }] });
const u = h.unit(1);
let cast = null;
h.b.on('skillStart', (c) => { if (c.unit === u) cast = h.b.time; });
assert.ok(h.runUntil(() => cast != null, 3));
h.step();
close(u.s.maxHp, u.base.maxHp * (1 + v), `${id}: max HP`);
close(u.s.def, u.base.def * (1 + v + passiveDefPct(id, u)), `${id}: DEF`);
h.runUntil(() => !u.skill.active, 45);
close(h.b.time - cast, 40, `${id}: 40 s`, 0.01);
close(u.s.maxHp, u.base.maxHp, `${id}: max HP back`);
checkInvariants(h.b);
}
});
test('S2 不变性原理 (星熊\'s chess): 20 s of DEF +50 % / +65 %; 反馈装甲 ×2 / ×3 on the enemies she blocks, back to −7 after', () => {
for (const [id, v, mult] of [['chess_char_4_17_a', 0.5, 2], ['chess_char_4_17_b', 0.65, 3]]) {
const h = battle({ units: [{ chessId: id, row: 10, col: 4, standIn: true }], enemies: [{ key: 'enemy_dummy', pos: [10, 4] }, { key: 'enemy_dummy', pos: [10, 4] }] });
const u = h.unit(1);
h.run(0.2);
assert.deepEqual(h.b.enemies.map((e) => e.s.aspd), [93, 93]);
let cast = null;
h.b.on('skillStart', (c) => { if (c.unit === u) cast = h.b.time; });
assert.ok(h.runUntil(() => cast != null, 60));
h.step();
assert.equal(u.skill.id, S2);
close(u.s.def, u.base.def * (1 + v + passiveDefPct(id, u)), `${id}: DEF`);
assert.deepEqual(h.b.enemies.map((e) => e.s.aspd), [100 - 7 * mult, 100 - 7 * mult], `${id}: ×${mult} at the cast`);
assert.ok(h.b.enemies.every((e) => !e.findBuff('acfend:feedback') && e.findBuff('acfend:feedback2')), `${id}: the plain one replaced`);
h.runUntil(() => !u.skill.active, 25);
close(h.b.time - cast, 20, `${id}: 20 s`, 0.01);
h.run(0.1 + TICK);
assert.deepEqual(h.b.enemies.map((e) => e.s.aspd), [93, 93], `${id}: plain again`);
checkInvariants(h.b);
}
});
test('S3 应力倒置 (号角 / 余\'s chess): 30 s of DEF +50 % / +70 %, block +1, ATK × 40 % / 55 % arts to each enemy she blocks every second', () => {
for (const [id, v, scale] of [['chess_char_5_08_a', 0.5, 0.4], ['chess_char_6_03_b', 0.7, 0.55]]) {
const h = battle({
units: [{ chessId: id, row: 10, col: 4, standIn: true, carryState: { sp: 999 } }],
enemies: [{ key: 'enemy_dummy', pos: [10, 4] }, { key: 'enemy_res', pos: [10, 4] }, { key: 'enemy_dummy', pos: [10, 4] }, { key: 'enemy_dummy', pos: [10, 4] }, { key: 'enemy_dummy', pos: [10, 4] }],
});
const u = h.unit(1);
let cast = null;
h.b.on('skillStart', (c) => { if (c.unit === u) cast = h.b.time; });
h.step();
assert.equal(u.blocking.length, 3, 'blocks 3');
assert.ok(h.runUntil(() => cast != null, 3));
h.run(0.5);
assert.equal(u.s.blockCnt, 4);
assert.equal(u.blocking.length, 4, `${id}: blocks a 4th`);
close(u.s.def, u.base.def * (1 + v + passiveDefPct(id, u)), `${id}: DEF`);
h.runUntil(() => !u.skill.active, 35);
close(h.b.time - cast, 30, `${id}: 30 s`, 0.01);
const pulses = h.dmg.filter((d) => d.src === u.id && d.tags.includes('stress'));
const times = [...new Set(pulses.map((d) => Math.round((d.t - cast) * 1000) / 1000))];
assert.equal(times.length, 30, `${id}: 30 pulses`);
times.forEach((t, i) => assert.ok(Math.abs(t - (i + 1)) <= TICK + 1e-6, `${id}: pulse ${i + 1} at ${t}`));
assert.ok(pulses.every((d) => d.type === 'arts' && d.skill));
const first = pulses.filter((d) => d.t === pulses[0].t);
assert.equal(first.length, 4, `${id}: each blocked enemy`);
for (const d of first) close(d.amount, u.s.atk * scale * (d.key === 'enemy_res' ? 0.8 : 1), `${id}: ${d.key}`);
h.run(0.2);
assert.equal(u.s.blockCnt, 3);
checkInvariants(h.b);
}
});
test('module PRO-X (5 号角 / 6 余 elites): DEF +20 % exactly while she blocks; none without it', () => {
for (const id of ['chess_char_4_17_b', 'chess_char_5_08_a', 'chess_char_5_08_b', 'chess_char_6_03_b']) {
const mod = moduleOf(id);
const h = battle({ units: [{ chessId: id, row: 10, col: 4, standIn: true }] });
const u = h.unit(1);
h.step();
close(u.s.def, u.base.def * 1.1, `${id}: not blocking`);
h.spawn('enemy_dummy', { pos: [10, 4] });
h.run(0.2);
assert.equal(u.blocking.length, 1);
close(u.s.def, u.base.def * (1.1 + (mod ? 0.2 : 0)), `${id}: blocking`);
assert.equal(!!mod, id === 'chess_char_5_08_b' || id === 'chess_char_6_03_b', id);
checkInvariants(h.b);
}
});