feedback5 WE2: larger-range triggers the data cannot carry — 谬因 S2's beam path, 薇薇安娜 S3's 3-2 from its second cast (#16)

The owner's ACTIVE_RANGE rule (a MANUAL skill whose running range strictly contains the
operator's own) for two skills whose running range is no data range (O1, O11 reports):
op-aphris.js adds the beam's path as a content trigger range; op-vvana.js switches S3 to
ACTIVE_RANGE on 3-2 once its first cast of a deployment ended (DEFAULT again on redeploy)
through a new additive SkillRuntime.setTrigger(rule, grid). The scan of the wave 2-3
reports and the 41 candidate skills is in the doc draft (others: target-centred, already
fixed, or owner questions). Golden: diy-045 (谬因 S2 fires for enemies beyond her 6 tiles)
and diy-078 (薇薇安娜 S3: skill timing in hooks / snapshots only) moved.
This commit is contained in:
sganggs committed 2026-10-06 16:01:48 +08:00
1 parent 2eaef154d5
commit 1559614e8a
6 files changed
+135 -25

No files matched your search

@@ -0,0 +1,87 @@
// test/content/feedback5-we2-area-triggers.test.js — 0.2.0 WE2 #16: kit-side larger-range triggers where no data range can
// carry the owner's ACTIVE_RANGE rule (2026-10-05: a MANUAL skill whose running range strictly contains the operator's own
// casts as soon as an enemy is inside that larger range):
// * 谬因 S2 临界瞬爆 — the beam's unbounded path (O1's report): content trigger range = the path she would fire along now;
// * 薇薇安娜 S3 “明灭” — from its second cast of a deployment on 3-2 (O11's report): ACTIVE_RANGE after the first cast ends
// (skills.js setTrigger), DEFAULT again at her next deployment.
// Run: node --test test/content/feedback5-we2-area-triggers.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
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' }) };
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
/** `charId` as uid 1 at (row, col) facing RIGHT with skill `skill`. */
function field(charId, { skill, row, col, tier = 5, seed = 5 }) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpInit: 99, dpPerSec: 0, dpMax: 999, startOpCooldown: 0 }, hooks: ['skillStart', 'skillEnd'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId, skillIndex: skill, uniEquipId: null }, elite: false, row, col }],
});
h.step();
return { h, u: h.unit(1) };
}
test('#16 谬因 S2: an enemy on the beam\'s line beyond her own 6 tiles casts it (data DEFAULT alone would wait); one off the line does not', () => {
// her own range (9, 2) … (9, 7); the beam reaches the end of the row
{
const { h, u } = field('char_4229_aphris', { skill: 1, row: 9, col: 2 });
assert.equal(u.skill.rule, 'DEFAULT');
assert.equal(u.skill.triggerRanges.length, 1, 'the beam path is a content trigger range');
h.spawn('enemy_dummy', { pos: [10, 12] }); // off the line
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, 'off the line: no cast');
h.spawn('enemy_dummy', { pos: [9, 13] }); // on the line, far beyond (9, 7)
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'on the beam\'s line: cast');
done(h);
}
// a flyer on the line too (the beam hits air units)
{
const { h, u } = field('char_4229_aphris', { skill: 1, row: 9, col: 2 });
h.spawn('enemy_fly', { pos: [9, 14] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === 1, 1));
done(h);
}
// S1 / S3 get no such range
for (const skill of [0, 2]) {
const { h, u } = field('char_4229_aphris', { skill, row: 9, col: 2 });
assert.equal(u.skill.triggerRanges.length, 0, `S${skill + 1}`);
done(h);
}
});
test('#16 薇薇安娜 S3: the first cast of a deployment by the data\'s DEFAULT; once it ended, an enemy on the 3-2 beyond her own reach casts the next one (ACTIVE_RANGE); a redeploy goes back to DEFAULT', () => {
const { h, u } = field('char_4098_vvana', { skill: 2, row: 10, col: 3 });
assert.equal(u.skill.rule, 'DEFAULT');
// (10, 6): on her 3-2 (cols 3–6), outside her own 1-1 (cols 3–4)
const far = h.spawn('enemy_dummy', { pos: [10, 6] });
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, 'first cast: DEFAULT on her own range — the far enemy does not cast it');
// an enemy in her own range: the first cast; then let it run out
const near = h.spawn('enemy_dummy', { pos: [10, 4] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), 'first cast');
assert.ok(h.runUntil(() => !u.skill.active, 30), 'it ends');
assert.deepEqual([u.skill.rule, u.skill.triggerGrid], ['ACTIVE_RANGE', [[0, 0], [0, 1], [0, 2], [0, 3]]], 'from now on the 3-2');
h.b.kill(near, null);
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === 2, 4), 'the far enemy alone casts the second one');
void far;
// a redeploy: the count and the rule start again
assert.ok(h.runUntil(() => !u.skill.active, 40));
h.b.retreat(u, { reason: 'retreat' });
h.step();
assert.ok(h.b.redeploy(u, { free: true }));
h.step();
assert.deepEqual([u.skill.rule, u.skill.triggerGrid], ['DEFAULT', null], 'the next deployment: the data\'s rule');
done(h);
});