卫戍协议:盟约 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).
This commit is contained in:
sganggs committed 2026-09-29 16:21:31 +08:00
commit 01a44cd288
408 files changed
+308986

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
// 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');
});