feedback5 WF: ACTIVE_RANGE also replaces the SEARCH row and covers 深巡 S2 — a skill whose running range strictly contains the own one casts with an enemy inside it (the owner's decisions of 2026-10-05)

tools/build-data.mjs resolveTrigger: a MANUAL operator skill on DEFAULT (no row, or a DEFAULT entry of the deviation
tables) or on the SEARCH row ("在初始攻击范围内存在敌人时释放技能": 解放者 / 阵法术师 / 安洁莉娜 — ACTIVE_RANGE_OVER) whose
running range strictly contains its own is ACTIVE_RANGE on that range, rawRule the official row; validateAll accepts a
DEFAULT deviation resolved to ACTIVE_RANGE (chess and stand-in tables) and ACTIVE_RANGE over SEARCH. Data rebuilt
offline, JSON-compared: only skills[].trigger rule / customRangeGrid of 薄绿 S1 (x-2), 蜜蜡 S1 (x-2), 卡涅利安 S3 (x-2),
玛恩纳 S2 (2-3), 安洁莉娜 S3 (y-4) — rawRule SEARCH — and 深巡 S2 (3-2, rawRule TAKE_DAMAGE), normal + elite (32
leaves); backups.json unchanged. Kits: 薄绿 S1 drops its DEFAULT trigger and 深巡 S2 PR #12's DEFAULT line, both read the
data now (雷蛇 S2 keeps PR #12's line). The sim needs no change (SEARCH was the DEFAULT condition on the initial range
every tick; ACTIVE_RANGE is the same on the running range). Docs: DESIGN §5.6 and the §21.20 flip row, DATA, SIM,
PLAYING, kits README, shared tier comments, skills.js header.

Tests: data (78 records / 39 skills, the five from SEARCH, the SEARCH skills that stay), feedback5-active-range (安洁莉娜
S3 on its y-4, S2 keeps SEARCH), feedback1-tank-triggers and kits_t1t2 (深巡 S2 on its 3-2, before any hit), kits_alt_t3
(薄绿 S1 on its x-2), feedback1f-fence (the idle measure uses the interval of the cooldown still running: the S2 now casts
between two attacks), docs-consistency (one PR #12 kit line left).

Golden: roster-001 / 002 (深巡), roster-016 / 044 (玛恩纳, 安洁莉娜), roster-026 / 027 (薄绿), roster-041 / 042 (卡涅利安),
bond-egirShip-low / bond-suntShip-high (深巡), boss-boss_2-pair / boss-boss_6-pair (玛恩纳, 安洁莉娜), matches solo-NORMAL-1,
coop2-NORMAL-3, coop4-FUNNY-6, coop2-NORMAL-8-serverrun — each fields one of the six skills, which now casts when an
enemy enters its running range.
This commit is contained in:
sganggs committed 2026-10-05 18:45:13 +08:00
1 parent 65ac4afbd8
commit 383fe4a49e
27 files changed
+463 -383

No files matched your search

+18 -9
View File
@@ -7,6 +7,9 @@
// One more since 2026-10-04 (the owner's decision after GitHub issue #32 item 1, DESIGN §22.10): 余 S2 厚礼上宾 casts with an
// enemy on its own 技能范围 x-1 — SKILL_RANGE, customRangeGrid its range (his attack range is his own tile, so the basic
// strategy could not see an enemy he can pull); `rawRule` keeps TAKE_DAMAGE.
// And since 0.2.0 (the owner's decision of 2026-10-05): 深巡 S2's running range 3-2 strictly contains her 2-2, so the
// owner's ACTIVE_RANGE rule applies on top of her DEFAULT deviation — she casts with an enemy in the 3-2 (rule
// ACTIVE_RANGE, customRangeGrid the 3-2, rawRule still TAKE_DAMAGE; test/sim/feedback5-active-range.test.js).
//
// The battle check is real: the chess records of data/chess.json with their kits, the stage 战场#01(下半)
// (act2autochess_m01), a real enemy (萨卡兹枯朽前锋, melee, 0.8 tiles/s) walking the lane from the gate (9,10) to the
@@ -32,6 +35,8 @@ const DEVIATED = [
['chess_char_2_18_a', 'skcom_atk_up[3]'], // 灰毫 S1 攻击力强化·γ型
['chess_char_2_18_a', 'skchr_ashlok_2'], // 灰毫 S2 专注轰击
];
/** The deviated skill whose running range strictly contains the own one: ACTIVE_RANGE on top (0.2.0). */
const ACTIVE = new Set(['skchr_udflow_2']);
/** 余 S2 厚礼上宾: SKILL_RANGE on its own x-1 (DESIGN §22.10). */
const SKILL_RANGE_DEVIATED = [['chess_char_6_03_a', 'skchr_yu_2']];
const both = (normalId) => [normalId, CHESS[normalId].goldenId];
@@ -70,16 +75,17 @@ function firstCast(chessId, skillId, { spawnAt = 0, seconds = 40 } = {}) {
return out;
}
test('data: the six deviated 重装 skills are DEFAULT and 余 S2 is SKILL_RANGE on its x-1, on the normal and the elite record, rawRule the official TAKE_DAMAGE', () => {
test('data: the six deviated 重装 skills are DEFAULT (深巡 S2: ACTIVE_RANGE on its 3-2) and 余 S2 is SKILL_RANGE on its x-1, on the normal and the elite record, rawRule the official TAKE_DAMAGE', () => {
let n = 0;
for (const [normalId, skillId] of DEVIATED) {
for (const id of both(normalId)) {
const s = CHESS[id].skills.find((x) => x.skillId === skillId);
assert.ok(s, `${id} ${skillId}`);
assert.equal(CHESS[id].profession, 'TANK');
assert.deepEqual(s.trigger, { rule: 'DEFAULT', rawRule: 'TAKE_DAMAGE', customRangeGrid: null }, `${id} ${skillId}`);
const want = ACTIVE.has(skillId) ? { rule: 'ACTIVE_RANGE', rawRule: 'TAKE_DAMAGE', customRangeGrid: s.rangeGrid } : { rule: 'DEFAULT', rawRule: 'TAKE_DAMAGE', customRangeGrid: null };
assert.deepEqual(s.trigger, want, `${id} ${skillId}`);
// the sim's loadout def carries the data rule
assert.equal(ds.getChess(id, { skillIndex: s.index }).skill.trigger.rule, 'DEFAULT', `${id} ${skillId} loadout def`);
assert.equal(ds.getChess(id, { skillIndex: s.index }).skill.trigger.rule, want.rule, `${id} ${skillId} loadout def`);
n++;
}
}
@@ -110,7 +116,7 @@ test('data: the six deviated 重装 skills are DEFAULT and 余 S2 is SKILL_RANGE
for (const c of others) assert.deepEqual(c.skills.find((s) => s.skillId === 'skcom_atk_up[3]').trigger, { rule: 'DEFAULT', rawRule: 'DEFAULT', customRangeGrid: null }, c.chessId);
});
test('PR #12\'s kit lines agree with the data: 深巡 / 雷蛇 S2 specs say DEFAULT, the other deviated skills read the data', async () => {
test('PR #12\'s kit line agrees with the data: 雷蛇 S2\'s spec says DEFAULT, the other deviated skills (深巡 S2 since 0.2.0) read the data', async () => {
const { KITS } = await import('../../server/sim/content/index.js');
for (const [normalId, skillId] of [...DEVIATED, ...SKILL_RANGE_DEVIATED]) {
for (const id of both(normalId)) {
@@ -121,21 +127,24 @@ test('PR #12\'s kit lines agree with the data: 深巡 / 雷蛇 S2 specs say DEFA
const spec = s.isDefault ? kit?.skill : kit?.skills?.[skillId];
const specRule = typeof spec?.trigger === 'string' ? spec.trigger : spec?.trigger?.rule;
assert.ok(specRule == null || specRule === s.trigger.rule, `${id} ${skillId}: kit ${specRule} vs data ${s.trigger.rule}`);
if (skillId === 'skchr_udflow_2' || skillId === 'skchr_liskam_2') assert.equal(specRule, 'DEFAULT', `${id} ${skillId}: PR #12's trigger line`);
if (skillId === 'skchr_liskam_2') assert.equal(specRule, 'DEFAULT', `${id} ${skillId}: PR #12's trigger line`);
if (skillId === 'skchr_udflow_2') assert.equal(specRule, undefined, `${id} ${skillId}: reads the data's ACTIVE_RANGE (PR #12's line would keep the 2-2)`);
}
}
});
test('real battle: each deviated skill casts with an enemy in range before anything hits the operator (normal + elite)', () => {
test('real battle: each deviated skill casts with an enemy in range before anything hits the operator (normal + elite; 深巡 S2 in its 3-2)', () => {
for (const [normalId, skillId] of DEVIATED) {
for (const id of both(normalId)) {
const r = firstCast(id, skillId, { spawnAt: 3 });
const where = `${id} ${skillId}: ${JSON.stringify(r)}`;
assert.equal(r.rule, 'DEFAULT', where);
const rule = ACTIVE.has(skillId) ? 'ACTIVE_RANGE' : 'DEFAULT';
assert.equal(r.rule, rule, where);
assert.ok(r.cast != null, `never cast — ${where}`);
assert.ok(r.cast >= 3, `no cast before an enemy is on the field — ${where}`);
assert.equal(r.reason, 'DEFAULT', where);
assert.ok(r.enemyInRange, `an enemy on its initial range at the cast — ${where}`);
assert.equal(r.reason, rule, where);
if (ACTIVE.has(skillId)) assert.ok(r.enemyInSkillRange && !r.enemyInRange, `an enemy on the 3-2, not yet on the 2-2, at the cast — ${where}`);
else assert.ok(r.enemyInRange, `an enemy on its initial range at the cast — ${where}`);
assert.ok(r.hurt == null || r.hurt > r.cast, `cast before the first hit — ${where}`);
}
}
+6 -4
View File
@@ -40,14 +40,16 @@ const laneDirs = (st, r, c) => DIRS.filter((d) => [[0, 1], [0, 2]].some(([dr, dc
/**
* One battle: the operator alone on (r, c) facing `dir` against a real round template. Returns its attacks, the ticks a
* targetable enemy stood on its range tiles, the longest wait for an attack while one did and it could act (seconds
* beyond its attack interval) and the ticks it blocked anything.
* beyond its attack interval — the longer of the interval when the wait began and now: her S2 casts on its 3-2 between
* two attacks since 0.2.0, the owner's ACTIVE_RANGE rule, and its ASPD shortens the interval while the cooldown the
* last attack set still runs) and the ticks it blocked anything.
*/
function watch(stageId, chessId, r, c, dir, { template = 'act1autochess_02', seconds = 45 } = {}) {
const h = makeBattle({ stageId, waveTemplate: template, units: [{ chessId, row: r, col: c, dir }], seed: 5 });
h.step(1);
const u = h.unit(chessId);
assert.ok(u && u.deployed && u.tileR === r && u.tileC === c, `${stageId} (${r},${c}) deployed`);
let attacks = 0, inRange = 0, blocked = 0, waitFrom = null, worst = 0;
let attacks = 0, inRange = 0, blocked = 0, waitFrom = null, waitIv = 0, worst = 0;
h.b.on('attack', (ctx) => { if (ctx.attacker === u) { attacks++; waitFrom = null; } });
for (let i = 0; i < seconds * 30 && !h.b.finished; i++) {
h.step(1);
@@ -58,8 +60,8 @@ function watch(stageId, chessId, r, c, dir, { template = 'act1autochess_02', sec
const target = h.b.enemies.some((e) => e.alive && e.deployed && canTargetEnemy(u, e, prof) && bodyInKeys(e, set));
if (!target) { waitFrom = null; continue; }
inRange++;
if (waitFrom == null) waitFrom = h.b.time;
worst = Math.max(worst, h.b.time - waitFrom - u.s.interval);
if (waitFrom == null) { waitFrom = h.b.time; waitIv = u.s.interval; }
worst = Math.max(worst, h.b.time - waitFrom - Math.max(waitIv, u.s.interval));
}
return { attacks, inRange, blocked, worst };
}
+26
View File
@@ -3,6 +3,9 @@
// as soon as an enemy (a heal skill: an injured ally) is inside that larger range — trigger ACTIVE_RANGE, resolved by
// tools/build-data.mjs resolveTrigger (customRangeGrid = the running range) and checked every tick by server/sim/skills.js.
// Community report: 「有的干员开技能后的攻击范围比平时攻击范围大,但是怪走到平时的攻击范围内才会开技能」.
// The owner's further decisions of the same day: the rule also replaces the SEARCH row ("在初始攻击范围内存在敌人时释放技能":
// 薄绿 S1, 蜜蜡 S1, 卡涅利安 S3, 玛恩纳 S2, 安洁莉娜 S3) and covers 深巡 S2 on top of its §21.29 DEFAULT (its 3-2 over her
// 2-2: test/sim/feedback1-tank-triggers.test.js).
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -91,3 +94,26 @@ test('real data: 莫斯提马 S3 序时之匙 (3-15 over her 3-6) casts on its r
assert.equal(casts(h, u)[0].reason, 'ACTIVE_RANGE');
done(h);
});
test('real data: the SEARCH row widened — 安洁莉娜 S3 秘杖·反重力模式 (y-4 over her y-2) casts on an enemy of the y-4 only, no attack needed; her S2 (no range change) keeps SEARCH on the y-2', () => {
const AGL = 'chess_char_5_20_a';
const idx = (sid) => ds.getChess(AGL).raw.skills.find((s) => s.skillId === sid).index;
const s3 = ds.getChess(AGL, { skillIndex: idx('skchr_aglina_3') }).skill, s2 = ds.getChess(AGL, { skillIndex: idx('skchr_aglina_2') }).skill;
assert.equal(s3.trigger.rule, 'ACTIVE_RANGE');
assert.deepEqual(s3.trigger.grid, s3.rangeGrid, 'the trigger grid is the y-4 she attacks with');
assert.equal(s2.trigger.rule, 'SEARCH');
// (10,6) = [0,3]: on the y-4, not on her y-2
for (const [sid, want] of [['skchr_aglina_3', 1], ['skchr_aglina_2', 0]]) {
const h = makeBattle({
defs: { enemies: { e: dummy('e') } }, units: [{ chessId: AGL, row: 10, col: 3, dir: 'RIGHT', skillIndex: idx(sid) }],
enemies: [{ key: 'e', pos: [10, 6] }], hooks: ['skillStart'], autoFinish: false, timeLimit: 30,
});
const u = h.unit(AGL);
h.step();
u.skill.gainSp(1000);
h.run(4);
assert.equal(casts(h, u).length, want, `${sid}: ${want ? 'cast on the y-4' : 'the enemy is outside the initial y-2'}`);
if (want) assert.equal(casts(h, u)[0].reason, 'ACTIVE_RANGE');
done(h);
}
});