Files
ark-stronghold-protocol/test/sim/docs-examples.test.js
T
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
2026-09-29 16:21:31 +08:00

114 lines
5.0 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.
// The worked examples of docs/SIM.md, executed (keeps the documentation honest).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
const dummy = (o = {}) => enemyRec({ key: 'enemy_dummy', hp: 1e7, speed: 0, ...o });
test('SIM.md §10: 隐现 S2 fires 14 shots then ends', () => {
const h = makeBattle({
defs: { enemies: { enemy_dummy: dummy() } },
units: [{ chessId: 'chess_char_1_01_a', row: 10, col: 4 }],
enemies: [{ key: 'enemy_dummy', pos: [10, 6] }],
hooks: ['ammoUsed', 'skillEnd'],
});
const u = h.unit('chess_char_1_01_a');
assert.ok(h.runUntil(() => u.skill.activations === 1, 60));
h.runUntil(() => !u.skill.active, 60);
assert.equal(h.hooksOf('ammoUsed').length, 14);
checkInvariants(h.b);
});
test('SIM.md example 1: ammo sniper kit with talent (+2 ammo after 20 s)', () => {
const kit = (bb, chess, def) => ({
skill: { kind: 'ammo', ammo: bb['attack@trigger_time'], mods: { atkPct: bb.atk, batPct: bb.base_attack_time, taunt: -1 }, targeting: { priority: 'ranged' } },
talents: [{ install(battle, unit) {
const t = def.talents[0].bb;
battle.on('skillStart', ({ unit: u, skill }) => { if (u === unit && battle.time - unit.deployedAt >= t.duration) skill.ammoLeft += t.self_ammo; }, { owner: unit });
} }],
});
const h = makeBattle({
defs: { enemies: { enemy_dummy: dummy() } }, kits: { chess_char_1_01_a: kit },
units: [{ chessId: 'chess_char_1_01_a', row: 10, col: 4 }], hooks: ['ammoUsed'],
});
const u = h.unit('chess_char_1_01_a');
h.run(25);
h.spawn('enemy_dummy', { pos: [10, 6] });
h.runUntil(() => u.skill.activations === 1, 10);
h.runUntil(() => !u.skill.active, 60);
assert.equal(h.hooksOf('ammoUsed').length, 14 + 2);
assert.equal(u.s.taunt, 0, 'taunt −1 removed after the skill');
});
test('SIM.md example 2: 幽灵鲨 undying during the skill, self-stun afterwards', () => {
const kit = (bb) => ({
skill: {
kind: 'duration', mods: { atkPct: bb.atk },
onStart({ battle, unit }) { unit.mem.undying = battle.on('fatal', (c) => { if (c.unit === unit) c.prevented = true; }, { owner: unit }); },
onEnd({ battle, unit, reason }) { battle.off(unit.mem.undying); if (reason !== 'death') battle.applyStatus(unit, 'stun', { duration: bb.stun, source: unit }); },
},
});
const h = makeBattle({
defs: { enemies: { enemy_dummy: dummy() } }, kits: { chess_char_2_07_a: kit },
units: [{ chessId: 'chess_char_2_07_a', row: 9, col: 5 }], enemies: [{ key: 'enemy_dummy', pos: [9, 6] }],
});
const u = h.unit('chess_char_2_07_a');
h.runUntil(() => u.skill.active, 60);
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
assert.equal(u.alive, true);
assert.ok(u.hp >= 1);
h.runUntil(() => !u.skill.active, 20);
h.step();
assert.ok(u.s.flags.stun);
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
assert.equal(u.alive, false, 'undying ended with the skill');
});
test('SIM.md example 5: 赫默 summons a medical drone that expires after 10 s', () => {
const kit = () => ({
skill: {
kind: 'instant', trigger: 'SP_FULL',
onStart({ battle, unit }) {
for (const k of unit.rangeKeys) {
const r = Math.floor(k / 21), c = k % 21;
if (!battle.unitAt(r, c) && battle.grid.canStand(r, c, { ranged: true })) { battle.spawnToken(unit, 'token_10000_silent_healrb', r, c, { duration: 10 }); break; }
}
},
},
});
const h = makeBattle({ kits: { chess_char_2_02_a: kit }, units: [{ chessId: 'chess_char_2_02_a', row: 10, col: 4 }] });
const u = h.unit('chess_char_2_02_a');
h.runUntil(() => u.skill.activations === 1, 60);
const drone = h.b.allyUnits.find((x) => x.kind === 'token');
assert.ok(drone && drone.alive);
assert.equal(drone.profile.dmgType, 'heal');
h.run(10.1);
assert.equal(drone.alive, false);
});
test('SIM.md example 6: 古米 TAKE_DAMAGE heal on the next attack', () => {
const kit = (bb) => ({
skill: {
kind: 'instant',
attack: { onHit({ battle, unit }) {
const ally = battle.alliesInRadius(unit.x, unit.y, 1.5, unit.ownerId).sort((a, b) => a.hpRatio - b.hpRatio)[0];
if (ally) battle.heal(unit, ally, unit.s.atk * bb.heal_scale);
} },
},
});
const h = makeBattle({
defs: { chess: { t_ally: chessRec({ id: 't_ally', skill: null, stats: { maxHp: 10000, atk: 0 } }) }, enemies: { enemy_dummy: dummy({ atk: 50, bat: 1 }) } },
kits: { chess_char_1_10_a: kit },
units: [{ chessId: 'chess_char_1_10_a', row: 9, col: 5 }, { chessId: 't_ally', row: 10, col: 5 }],
enemies: [{ key: 'enemy_dummy', pos: [9, 5], time: 8 }], autoFinish: false, // spawns on 古米's tile ⇒ blocked
});
const g = h.unit('chess_char_1_10_a');
h.run(7);
h.unit('t_ally').hp = 2000;
assert.equal(g.skill.rule, 'TAKE_DAMAGE');
assert.equal(g.skill.activations, 0, 'ready but not hit yet');
h.run(4);
assert.ok(g.skill.activations >= 1);
assert.ok(h.unit('t_ally').hp > 2000, 'healed by the empowered attack');
});