Files
sganggs ba66c2c32e feedback5 O3: 自选 operator kits for 逻各斯, 真言 and 维伊 (every skill, talent, trait and module at stages 1 / 3), the engine hook palsyTrigger
- kits/ops/op-logos.js (中坚术师; CCR-Δ, CCR-Y), op-mantra.js (本源术师; PRI-X), op-veen.js (秘术师; MSC-Y), registered in
  OPERATOR_KIT_FILES one line per file; numbers from data/backups.json, mechanics from the PRTS 备注 (cited in each header),
  [ASSUMED] items marked there. Data triggers kept; 维伊 S1 (an AUTO self buff) fires at full SP (the owner's rule).
- server/sim/ai.js: enemyAttack emits `palsyTrigger { enemy, buff, keep }` when a 麻痹 stack interrupts an attack (PRTS 真言
  备注 "触发麻痹时指的是敌人的攻击被麻痹打断时"); `keep` leaves the stack (真言 噤声限域). Additive: no listener, no change.
  docs/SIM.md §3 / §5 rows; test/sim/palsy-trigger.test.js.
- Tests: test/content/op_logos.test.js, op_mantra.test.js, op_veen.test.js (every form, skill, talent, module, range,
  trigger, targeting, damage type).
- Golden: only the diy family moved — diy-003 … diy-007 now field 逻各斯 / 真言 / 维伊 in every form × module × skill (60
  configurations; diy-003 … 006 held the prototypes before), the prototype picks moved to diy-008 … 011 (same packing, new
  ids); diy-001 / 002 (推进之王) and roster, bonds, fields, matches, standins byte-identical.
2026-10-06 00:04:18 +08:00

63 lines
2.8 KiB
JavaScript

// test/sim/palsy-trigger.test.js — the engine hook `palsyTrigger` (server/sim/ai.js enemyAttack): fired when a 麻痹 stack
// interrupts an enemy's normal attack (PRTS 真言 备注 "触发麻痹时指的是敌人的攻击被麻痹打断时"), once per interrupt; a
// handler may keep the stack (`keep`) or kill the enemy. 真言's 噤声限域 (kits/ops/op-mantra.js) is its user.
// Run: node --test test/sim/palsy-trigger.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
const guard = chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 0, blockCnt: 2, maxHp: 1e6 }, skill: null });
const shooter = enemyRec({ key: 'enemy_shooter', hp: 1e6, atk: 100, bat: 1, range: 3, speed: 0, mass: 0 });
function field(setup) {
const h = makeBattle({
defs: { chess: { t_guard: guard }, enemies: { enemy_shooter: shooter } },
units: [{ chessId: 't_guard', row: 10, col: 5 }], content: 'none', autoFinish: false, setup,
});
h.step();
return h;
}
test('palsyTrigger: one event per interrupted attack, before the stack is used; no event without 麻痹', () => {
const seen = [];
const h = field((b) => b.on('palsyTrigger', (c) => seen.push({ t: b.time, stacks: c.buff.stacks, enemy: c.enemy })));
const e = h.spawn('enemy_shooter', { pos: [10, 7] });
h.run(3.05);
assert.equal(seen.length, 0, 'no 麻痹, no event');
const a0 = e.stats.attacks;
h.b.applyStatus(e, 'palsy', { value: 2 });
h.run(2.05);
assert.deepEqual(seen.map((x) => [x.enemy, x.stacks]), [[e, 2], [e, 1]], 'each interrupt, the stack count before it is used');
assert.equal(e.stats.attacks, a0, 'both attacks interrupted');
assert.equal(e.findBuff('palsy'), null);
h.run(1.1);
assert.ok(e.stats.attacks > a0, 'attacks again');
checkInvariants(h.b);
});
test('palsyTrigger: `keep` leaves the stack; a handler that kills the enemy ends its turn', () => {
let keepNext = true;
const h = field((b) => b.on('palsyTrigger', (c) => { if (keepNext) { c.keep = true; keepNext = false; } }));
const e = h.spawn('enemy_shooter', { pos: [10, 7] });
h.step();
h.b.applyStatus(e, 'palsy', { value: 1 });
const a0 = e.stats.attacks;
h.run(1.1);
assert.equal(e.findBuff('palsy')?.stacks, 1, 'kept');
assert.equal(e.stats.attacks, a0, 'still interrupted');
h.run(1.05);
assert.equal(e.findBuff('palsy'), null, 'used by the next interrupt');
assert.equal(e.stats.attacks, a0);
// a lethal handler
const h2 = field((b) => b.on('palsyTrigger', (c) => b.kill(c.enemy, null)));
const e2 = h2.spawn('enemy_shooter', { pos: [10, 7] });
h2.step();
h2.b.applyStatus(e2, 'palsy', { value: 3 });
h2.run(1.2);
assert.ok(!e2.alive);
assert.equal(h2.b.errors.length, 0, JSON.stringify(h2.b.errors[0]));
checkInvariants(h.b);
checkInvariants(h2.b);
});