Files
ark-stronghold-protocol/test/content/feedback5-wv-summon-triggers.test.js
T

124 lines
6.6 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/feedback5-wv-summon-triggers.test.js — 0.2.0 WV #2: the owner's larger-range rule through summons (the
// owner's decision of 2026-10-06: "a skill whose effect reaches farther than the operator's own range casts when an enemy is
// inside the larger area"). A skill that acts through its owner's summons casts on an enemy inside such an area of a summon
// standing on the field, on top of its data rule (DEFAULT: an enemy in the owner's own range) — kits/shared/summoner.js
// summonTriggerArea (a content trigger range, server/sim/skills.js addTriggerRange with a `{ keys, profile }` entry):
// * 麦哲伦 S1 高效制冷模块 — her drones' ranges (air units too: its 停顿 / 束缚 reach them);
// * 令 S3 宁作吾 — each summon's x-5 (ground enemies only: its pulses "不可对空");
// * 电弧 S2 环形鳞地 — 赛柯's range (ground only: its bullets reach no air unit); S3 手牵手 — 桑特拉's range (air too).
// Run: node --test test/content/feedback5-wv-summon-triggers.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { COLS } from '../../server/sim/constants.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' }) };
const MG = 'char_248_mgllan', LING = 'char_2023_ling', RADIAN = 'char_4195_radian';
const DRONE_F = 'token_10005_mgllan_drone1';
const SOUL3 = 'token_10020_ling_soul3';
const TOWER2 = 'token_10052_radian_tower2', TOWER3 = 'token_10053_radian_tower3';
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
/** `charId` as uid 1 at (9, 2) facing RIGHT (her 3-1: rows 8–10, cols 2–5) with skill `skill`; `pieces` = [[tokenId, r, c]]. */
function field(charId, skill, pieces = [], { tier = 6, elite = true } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed: 5,
flags: { dpInit: 99, dpPerSec: 0, dpMax: 999, startOpCooldown: 0 }, hooks: ['skillStart', 'skillEnd', 'death'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId, skillIndex: skill, uniEquipId: null }, elite, row: 9, col: 2 },
...pieces.map(([tokenId, r, c], i) => ({ uid: 10 + i, kind: 'token', tokenId, ownerUid: 1, row: r, col: c, dir: 'RIGHT' }))],
});
h.step();
const u = h.unit(1);
return { h, u, ps: h.b.allyUnits.filter((a) => a.kind === 'token' && a.ownerUnit === u) };
}
/** Fill the skill and run `s` seconds; true when it was cast. */
function casts(h, u, s = 2) {
u.skill.gainSp(999);
h.run(s);
return u.skill.activations > 0;
}
const outOfHerRange = (u, r, c) => !u.rangeKeySet.has(r * COLS + c);
test('#2 麦哲伦 S1: an enemy on a standing drone\'s range (air units too) casts it; none there or with the drone gone: no cast; S2 / S3 get no such area', () => {
{ // a ground enemy on 龙腾.F's x-4, far from her 3-1
const { h, u, ps } = field(MG, 0, [[DRONE_F, 12, 9]]);
assert.equal(u.skill.rule, 'DEFAULT');
assert.ok(ps[0].alive && ps[0].rangeKeySet.has(12 * COLS + 10) && outOfHerRange(u, 12, 10), 'the setup');
h.spawn('enemy_dummy', { pos: [11, 13] }); // outside every area
assert.ok(!casts(h, u), 'nobody in her range or a drone\'s: no cast');
h.spawn('enemy_dummy', { pos: [12, 10] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'an enemy on the drone\'s range casts S1');
done(h);
}
{ // a flyer there too
const { h, u } = field(MG, 0, [[DRONE_F, 12, 9]]);
h.spawn('enemy_fly', { pos: [11, 10] });
assert.ok(casts(h, u), 'a flyer on the drone\'s range');
done(h);
}
{ // the drone off the field: nothing
const { h, u, ps } = field(MG, 0, [[DRONE_F, 12, 9]]);
h.b.retreat(ps[0], { reason: 'retreat' });
h.spawn('enemy_dummy', { pos: [12, 10] });
assert.ok(!casts(h, u), 'no standing drone: no cast');
done(h);
}
for (const skill of [1, 2]) {
const { h, u } = field(MG, skill, []);
assert.equal(u.skill.triggerRanges.length, 0, `S${skill + 1}`);
done(h);
}
});
test('#2 令 S3: a ground enemy on a standing summon\'s x-5 casts it; a flyer there, or a ground enemy off the x-5, does not; S1 / S2 get no such area', () => {
{ // 弦惊 at (12, 9): its x-5 = (12, 9) and its four side neighbours
const { h, u, ps } = field(LING, 2, [[SOUL3, 12, 9]]);
assert.equal(u.skill.rule, 'DEFAULT');
assert.ok(ps[0].alive && outOfHerRange(u, 11, 9), 'the setup');
h.spawn('enemy_fly', { pos: [11, 9] }); // on the x-5, but air
h.spawn('enemy_dummy', { pos: [11, 10] }); // diagonal: off the x-5
assert.ok(!casts(h, u), 'a flyer on the x-5 / a ground enemy off it: no cast');
h.spawn('enemy_dummy', { pos: [11, 9] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'a ground enemy on the summon\'s x-5 casts S3');
done(h);
}
for (const skill of [0, 1]) {
const { h, u } = field(LING, skill, []);
assert.equal(u.skill.triggerRanges.length, 0, `S${skill + 1}`);
done(h);
}
});
test('#2 电弧 S2: a ground enemy on 赛柯\'s range casts it (a flyer there does not); S3: an enemy on 桑特拉\'s range, air units too; S1 gets no such area', () => {
{ // 赛柯 at (12, 6) facing RIGHT: its 3-2 = (12, 6) … (12, 9)
const { h, u, ps } = field(RADIAN, 1, [[TOWER2, 12, 6]]);
assert.equal(u.skill.rule, 'DEFAULT');
assert.ok(ps[0].alive && ps[0].rangeKeySet.has(12 * COLS + 9) && outOfHerRange(u, 12, 9), 'the setup');
h.spawn('enemy_fly', { pos: [12, 9] });
assert.ok(!casts(h, u), 'a flyer on 赛柯\'s range: no cast');
h.spawn('enemy_dummy', { pos: [12, 9] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'a ground enemy on 赛柯\'s range casts S2');
done(h);
}
{ // 桑特拉 at (12, 6) facing RIGHT: its 3-1 = rows 11–13, cols 6–9
const { h, u, ps } = field(RADIAN, 2, [[TOWER3, 12, 6]]);
assert.ok(ps[0].alive && ps[0].rangeKeySet.has(12 * COLS + 9) && outOfHerRange(u, 12, 9), 'the setup');
h.spawn('enemy_dummy', { pos: [11, 13] }); // outside every area
assert.ok(!casts(h, u), 'nobody in an area: no cast');
h.spawn('enemy_fly', { pos: [12, 9] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'a flyer on 桑特拉\'s range casts S3');
done(h);
}
{
const { h, u } = field(RADIAN, 0, []);
assert.equal(u.skill.triggerRanges.length, 0, 'S1');
done(h);
}
});