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

+30 -15
View File
@@ -452,6 +452,9 @@ const ATTACK_RANGE_CHANGE = /攻击(?:范围|距离)(?:与溅射范围)?(?:扩
* from a ranged hit with nobody to pull). His attack range is his own tile (0-1), so the basic strategy would not see an
* enemy he could pull: his S2 takes SKILL_RANGE on its own 技能范围 (x-1) — the official strategy of a MANUAL skill with a
* 技能范围 when no class row applies, "仅在技能范围内存在敌人(无视其不可选中)时释放技能" — `customRangeGrid` = that range.
* A DEFAULT entry is the basic strategy, so the owner's ACTIVE_RANGE rule applies on top of it (resolveTrigger; the
* owner's decision of 2026-10-05): 深巡 S2, whose running range 3-2 strictly contains her 2-2, casts with an enemy in the
* 3-2 (`rule` ACTIVE_RANGE, `rawRule` still TAKE_DAMAGE).
*/
const TRIGGER_DEVIATIONS = Object.freeze({
chess_char_1_04_a: { skchr_udflow_2: 'DEFAULT' }, // 深巡 S2 行动能力剥夺
@@ -494,6 +497,14 @@ function strictlyContains(a, b) {
return sa.size > sb.size && [...sb].every((k) => sa.has(k));
}
/**
* The rules the owner's ACTIVE_RANGE replaces (resolveTrigger), both of which wait for an enemy in the INITIAL range:
* DEFAULT, the basic strategy (no row, or a DEFAULT deviation of the tables — 深巡 S2, the owner's decision of
* 2026-10-05), and SEARCH, the 解放者 / 阵法术师 / 安洁莉娜 S2·S3 row "不受基础策略影响,在初始攻击范围内存在敌人时释放技能"
* (the owner's decision of 2026-10-05 too).
*/
const ACTIVE_RANGE_OVER = new Set(['DEFAULT', 'SEARCH']);
/**
* Resolve the auto-cast rule of a skill record (PRTS 卫戍协议/帮助 §作战阶段 技能操作 — the official skill strategies;
* DESIGN §5.6):
@@ -511,11 +522,13 @@ function strictlyContains(a, b) {
* unit and skill): `rule` from the table, `rawRule` the official row (a SKILL_RANGE deviation takes the skill's own
* range as `customRangeGrid`);
* - and the owner's rule of 2026-10-05, a deliberate deviation like §21.29 (community report 「有的干员开技能后的攻击范围比
* 平时攻击范围大,但是怪走到平时的攻击范围内才会开技能」): an operator's MANUAL skill left on the basic strategy (DEFAULT,
* no deviation of the table) whose attack range while it runs (activeAttackGrid) strictly contains the operator's own
* range casts as soon as an enemy — a heal skill: an injured ally — is inside that larger range: ACTIVE_RANGE,
* `customRangeGrid` = the running range, `rawRule` the official row. Other rows keep their rule (TAKE_DAMAGE waits for a
* hit, SEARCH reads the initial range by its official row, CUSTOM_RANGE / SP_FULL / … have their own).
* 平时攻击范围大,但是怪走到平时的攻击范围内才会开技能」): an operator's MANUAL skill on the basic strategy (DEFAULT — by
* no row, or by a DEFAULT deviation of the tables: 深巡 S2's 3-2 over her 2-2) or on the SEARCH row (解放者 / 阵法术师 /
* 安洁莉娜: 薄绿 S1, 蜜蜡 S1, 卡涅利安 S3, 玛恩纳 S2, 安洁莉娜 S3) — ACTIVE_RANGE_OVER, both by the owner's decisions
* of 2026-10-05 — whose attack range while it runs (activeAttackGrid) strictly contains the operator's own range casts as
* soon as an enemy — a heal skill: an injured ally — is inside that larger range: ACTIVE_RANGE, `customRangeGrid` = the
* running range, `rawRule` the official row. Other rows keep their rule (TAKE_DAMAGE waits for a hit, CUSTOM_RANGE /
* SKILL_RANGE / SP_FULL / … have their own range or none).
* @param {object} skill record from buildSkill (skillId, skillType, desc, rangeGrid, bb)
* @param {{operator?: boolean, chessId?: string, unitCharId?: string, baseGrid?: number[][]|null}} opts operator = a
* chess (the 技能范围 strategy is written for 干员; summons keep DEFAULT); chessId = the chess's NORMAL id
@@ -540,12 +553,12 @@ function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false,
if (!skill.rangeGrid) warn(`trigger deviation ${chessId} ${skill.skillId}: SKILL_RANGE without a 技能范围`);
return { rule: deviation, rawRule, customRangeGrid: skill.rangeGrid ? skill.rangeGrid.map((p) => p.slice()) : null };
}
if (deviation) return { rule: deviation, rawRule, customRangeGrid: null };
const rule = TRIGGER_RENAME[rawRule] || rawRule;
if (rule === 'DEFAULT' && operator && manual && baseGrid?.length) {
const rule = deviation || TRIGGER_RENAME[rawRule] || rawRule;
if (ACTIVE_RANGE_OVER.has(rule) && operator && manual && baseGrid?.length) {
const active = activeAttackGrid(skill, baseGrid);
if (active && strictlyContains(active, baseGrid)) return { rule: 'ACTIVE_RANGE', rawRule, customRangeGrid: active };
}
if (deviation) return { rule: deviation, rawRule, customRangeGrid: null };
let customRangeGrid = null;
if (rawRule === 'CUSTOM_RANGE_SEARCH_ENEMY') {
const rid = ctx.ac.skillRangeDict?.[skill.skillId];
@@ -3425,7 +3438,8 @@ function validateAll(f) {
if (c.placement !== undefined) err(`chess ${c.chessId}: placement is not a data field`);
}
// the deliberate trigger deviations (DESIGN §21.29, §22.10) still override an official TAKE_DAMAGE row, on the normal
// chess and its elite alike
// chess and its elite alike (a DEFAULT one may resolve to the owner's ACTIVE_RANGE on top: 深巡 S2)
const deviationHolds = (rule, trig) => trig.rawRule === 'TAKE_DAMAGE' && (trig.rule === rule || (ACTIVE_RANGE_OVER.has(rule) && trig.rule === 'ACTIVE_RANGE'));
for (const [baseId, skillsOf] of Object.entries(TRIGGER_DEVIATIONS)) {
const recs = Object.values(chess).filter((c) => c.baseId === baseId);
if (recs.length !== 2) err(`trigger deviation ${baseId}: expected the normal and the elite record, got ${recs.length}`);
@@ -3433,18 +3447,19 @@ function validateAll(f) {
for (const c of recs) {
const s = (c.skills || []).find((x) => x.skillId === skillId);
if (!s) err(`trigger deviation ${c.chessId}: no skill ${skillId}`);
else if (s.trigger.rawRule !== 'TAKE_DAMAGE' || s.trigger.rule !== rule) err(`trigger deviation ${c.chessId} ${skillId}: ${s.trigger.rawRule} → ${s.trigger.rule}, expected TAKE_DAMAGE → ${rule}`);
else if (!deviationHolds(rule, s.trigger)) err(`trigger deviation ${c.chessId} ${skillId}: ${s.trigger.rawRule} → ${s.trigger.rule}, expected TAKE_DAMAGE → ${rule}`);
else if (rule === 'SKILL_RANGE' && (!s.rangeGrid?.length || JSON.stringify(s.trigger.customRangeGrid) !== JSON.stringify(s.rangeGrid))) err(`trigger deviation ${c.chessId} ${skillId}: SKILL_RANGE needs the skill's own range as customRangeGrid`);
}
}
}
// the owner's ACTIVE_RANGE rule (2026-10-05): a MANUAL skill of the basic strategy whose running range strictly contains
// the record's own range, and no other
// the owner's ACTIVE_RANGE rule (2026-10-05): a MANUAL skill of the basic strategy (DEFAULT by its row or a deviation)
// or of the SEARCH row whose running range strictly contains the record's own range, and no other
for (const c of Object.values(chess)) {
for (const s of c.skills || []) {
if (s.trigger?.rule !== 'ACTIVE_RANGE') continue;
if (s.skillType !== 'MANUAL' || s.trigger.rawRule !== 'DEFAULT' || !c.rangeGrid?.length || !strictlyContains(s.trigger.customRangeGrid || [], c.rangeGrid)) {
err(`chess ${c.chessId} ${s.skillId}: ACTIVE_RANGE needs a MANUAL basic-strategy skill whose customRangeGrid strictly contains the record's range`);
const over = TRIGGER_DEVIATIONS[c.baseId]?.[s.skillId] || s.trigger.rawRule;
if (s.skillType !== 'MANUAL' || !ACTIVE_RANGE_OVER.has(over) || !c.rangeGrid?.length || !strictlyContains(s.trigger.customRangeGrid || [], c.rangeGrid)) {
err(`chess ${c.chessId} ${s.skillId}: ACTIVE_RANGE needs a MANUAL basic-strategy or SEARCH skill whose customRangeGrid strictly contains the record's range`);
}
}
}
@@ -3479,7 +3494,7 @@ function validateAll(f) {
for (const f of forms) {
const s = f.skills.find((x) => x.skillId === skillId);
if (!s) err(`stand-in trigger deviation ${charId}@${statusKey(f.status)}: no skill ${skillId}`);
else if (s.trigger.rawRule !== 'TAKE_DAMAGE' || s.trigger.rule !== rule) err(`stand-in trigger deviation ${charId}@${statusKey(f.status)} ${skillId}: ${s.trigger.rawRule} → ${s.trigger.rule}, expected TAKE_DAMAGE → ${rule}`);
else if (!deviationHolds(rule, s.trigger)) err(`stand-in trigger deviation ${charId}@${statusKey(f.status)} ${skillId}: ${s.trigger.rawRule} → ${s.trigger.rule}, expected TAKE_DAMAGE → ${rule}`);
}
}
}