feedback5 WC: a MANUAL skill whose running attack range strictly contains the own range casts as soon as an enemy is inside it (ACTIVE_RANGE, the owner's rule of 2026-10-05)
A deliberate deviation from the official 技能策略 (community report: skills with a larger range waited for the normal range). tools/build-data.mjs resolveTrigger: a MANUAL operator skill still on the basic strategy (no class row, no TRIGGER_DEVIATIONS entry) whose running range (its rangeId grid, or the own range grown by ability_range_forward_extend; never 被动效果:攻击范围扩大) strictly contains the operator's range gets rule ACTIVE_RANGE, customRangeGrid = that range, rawRule the official DEFAULT; validateAll checks it. server/sim/skills.js: ACTIVE_RANGE checks the DEFAULT condition (a targetable enemy or a blocked one; a heal skill an injured ally) on that range, grown by the permanent rangeExtend, every tick. Data rebuilt offline: only skills[].trigger changed (33 skills, 66 chess records, 9 stand-in form records). 深巡 S2 (§21.29), 重装 TAKE_DAMAGE, the SEARCH / CUSTOM_RANGE / SP_FULL rows, 烛煌 S2 / S3 (not containing her 3-1) and 引星棘刺 S3 keep their rule. Docs: DESIGN §5.6, DATA, SIM, PLAYING, kits README, notes. Tests: new test/sim/feedback5-active-range.test.js; data, playtest6-skills (银灰 S3), kits_alt_t2 (蒂比 S1), kits_t5 (百炼嘉维尔 S2) updated; feedback1-transform's fixtures move 莫斯提马 one tile (her S3 now casts on its 3-15). Golden: 27 roster, 25 bond, 21 field and 14 match scenarios moved — every one fields an operator with one of the 33 skills, which now casts when an enemy enters its running range instead of the initial range.
This commit is contained in:
1 parent
ebfda7e657
commit
01fddd3021
23 files changed
+1921
-1708
No files matched your search
+49
-7
@@ -38,6 +38,7 @@ import { isDeepStrictEqual } from 'node:util';
|
||||
import { Grid, DEPLOY_REFUSED_TILES } from '../server/sim/grid.js';
|
||||
import { bandBondIds } from '../shared/bandBonds.js';
|
||||
import { statusKey, composeUnitRecord, standInRecord, unitForm } from '../shared/standIn.js';
|
||||
import { extendedGrid } from '../shared/loadoutRecord.js';
|
||||
|
||||
// ===== CLI & IO ==================================================================================
|
||||
|
||||
@@ -460,6 +461,26 @@ const TRIGGER_DEVIATIONS = Object.freeze({
|
||||
chess_char_6_03_a: { skchr_yu_2: 'SKILL_RANGE' }, // 余 S2 厚礼上宾 (its x-1)
|
||||
});
|
||||
|
||||
/**
|
||||
* The attack range a MANUAL skill gives while it runs, when its text changes the attack range (ATTACK_RANGE_CHANGE): its
|
||||
* own rangeId grid, else the operator's range grown by `ability_range_forward_extend` ("攻击距离+N": 空弦 S3, 史尔特尔
|
||||
* S2 / S3, 异客 S2 — the kits' targeting.rangeExtend, extendedGrid as Battle._refreshRange); null for any other skill and
|
||||
* for "被动效果:攻击范围扩大" (引星棘刺 S3: her own range while she carries it — shared/loadoutRecord.js attackRangeGrid).
|
||||
*/
|
||||
function activeAttackGrid(skill, baseGrid) {
|
||||
const desc = skill.desc || '';
|
||||
if (!ATTACK_RANGE_CHANGE.test(desc) || /被动效果:攻击范围扩大/.test(desc)) return null;
|
||||
if (skill.rangeGrid?.length) return skill.rangeGrid.map((p) => p.slice());
|
||||
const n = Math.round(Number(skill.bb?.ability_range_forward_extend) || 0);
|
||||
return n > 0 && baseGrid?.length ? extendedGrid(baseGrid, n) : null;
|
||||
}
|
||||
|
||||
/** True when grid `a` holds every tile of grid `b` and at least one more (`[dRow, dCol]` pairs, facing RIGHT). */
|
||||
function strictlyContains(a, b) {
|
||||
const sa = new Set(a.map(([r, c]) => `${r},${c}`)), sb = new Set(b.map(([r, c]) => `${r},${c}`));
|
||||
return sa.size > sb.size && [...sb].every((k) => sa.has(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the auto-cast rule of a skill record (PRTS 卫戍协议/帮助 §作战阶段 技能操作 — the official skill strategies;
|
||||
* DESIGN §5.6):
|
||||
@@ -474,12 +495,19 @@ const TRIGGER_DEVIATIONS = Object.freeze({
|
||||
* "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能", customRangeGrid = the skill range;
|
||||
* - else DEFAULT (the basic strategy: ready + about to attack / heal);
|
||||
* - last, the deliberate deviations (TRIGGER_DEVIATIONS, per chess and skill): `rule` from the table, `rawRule` the
|
||||
* official row (a SKILL_RANGE deviation takes the skill's own range as `customRangeGrid`).
|
||||
* @param {object} skill record from buildSkill (skillId, skillType, desc, rangeGrid)
|
||||
* @param {{operator?: boolean, chessId?: string}} opts operator = a chess (the 技能范围 strategy is written for 干员;
|
||||
* summons keep DEFAULT); chessId = the chess's NORMAL id (TRIGGER_DEVIATIONS key)
|
||||
* 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).
|
||||
* @param {object} skill record from buildSkill (skillId, skillType, desc, rangeGrid, bb)
|
||||
* @param {{operator?: boolean, chessId?: string, baseGrid?: number[][]|null}} opts operator = a chess (the 技能范围
|
||||
* strategy is written for 干员; summons keep DEFAULT); chessId = the chess's NORMAL id (TRIGGER_DEVIATIONS key);
|
||||
* baseGrid = the operator's own range at that status (ACTIVE_RANGE)
|
||||
*/
|
||||
function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false, chessId = null } = {}) {
|
||||
function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false, chessId = null, baseGrid = null } = {}) {
|
||||
const rows = Object.values(ctx.ac.skillTriggerDataList || {});
|
||||
const manual = skill.skillType === 'MANUAL';
|
||||
const pick =
|
||||
@@ -498,6 +526,10 @@ function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false,
|
||||
}
|
||||
if (deviation) return { rule: deviation, rawRule, customRangeGrid: null };
|
||||
const rule = TRIGGER_RENAME[rawRule] || rawRule;
|
||||
if (rule === 'DEFAULT' && operator && manual && baseGrid?.length) {
|
||||
const active = activeAttackGrid(skill, baseGrid);
|
||||
if (active && strictlyContains(active, baseGrid)) return { rule: 'ACTIVE_RANGE', rawRule, customRangeGrid: active };
|
||||
}
|
||||
let customRangeGrid = null;
|
||||
if (rawRule === 'CUSTOM_RANGE_SEARCH_ENEMY') {
|
||||
const rid = ctx.ac.skillRangeDict?.[skill.skillId];
|
||||
@@ -870,7 +902,7 @@ function buildChess(ctx) {
|
||||
if (!se?.skillId || (i !== sIdx && !unlocked(se.unlockCond, phase, level))) return;
|
||||
const s = buildSkill(ctx, se.skillId, skillLevel, null, `chess ${chessId}`);
|
||||
if (!s) return;
|
||||
s.trigger = resolveTrigger(ctx, char, shop.charId, i, s, { operator: true, chessId: baseId });
|
||||
s.trigger = resolveTrigger(ctx, char, shop.charId, i, s, { operator: true, chessId: baseId, baseGrid: rec.rangeGrid });
|
||||
s.index = i;
|
||||
s.overrideTokenKey = se.overrideTokenKey || null;
|
||||
skillRecs.push(s);
|
||||
@@ -1063,7 +1095,7 @@ function buildUnitForm(ctx, charId, status, { chessId = null, skillIndex = null
|
||||
if (!se?.skillId || (i !== skillIndex && !unlocked(se.unlockCond, phase, level))) return;
|
||||
const s = buildSkill(ctx, se.skillId, skillLevel, null, label);
|
||||
if (!s) return;
|
||||
s.trigger = resolveTrigger(ctx, char, charId, i, s, { operator: true, chessId });
|
||||
s.trigger = resolveTrigger(ctx, char, charId, i, s, { operator: true, chessId, baseGrid: rangeGrid(ctx, rangeId) });
|
||||
s.index = i;
|
||||
s.overrideTokenKey = se.overrideTokenKey || null;
|
||||
skills.push(s);
|
||||
@@ -3389,6 +3421,16 @@ function validateAll(f) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 补位 / 自选 (data/backups.json, DATA.md §18): a PRESET chess is its own backup; every NORMAL chess (both forms)
|
||||
// composes into its stand-in (shared/standIn.js standInRecord) with the chess's skill and module; every DIY slot is a
|
||||
// DIY chess and every prototype pick of its tier has a form for both slot statuses; derived bonds exist
|
||||
|
||||
Reference in new issue
Block a user