feedback1 final: six 重装 skills cast with an enemy in range, a deliberate deviation from the official TAKE_DAMAGE row (GitHub issue #4, PR #12)

The owner's decision (2026-10-03, after community feedback): 深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令
and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击 take the basic strategy (DEFAULT) instead of the 下半 TANK class
row's TAKE_DAMAGE. Every other MANUAL 重装 skill, 深巡 S1 included, still waits for a hit.

- tools/build-data.mjs TRIGGER_DEVIATIONS: a small table keyed by the normal chess id and the skill id (the elite
  follows through baseId), never by the skill id alone - 灰毫 S1 is the generic skcom_atk_up[3]. resolveTrigger applies
  it last; rawRule keeps the official TAKE_DAMAGE; validateAll fails the build when an entry no longer meets a
  TAKE_DAMAGE row. data/chess.json rebuilt with --offline: exactly the 12 records' rule changes.
- PR #12's kit lines (trigger 'DEFAULT' on 深巡 / 雷蛇 S2) stay and agree with the data; their comments and tests now
  give this reason instead of the community summary.
- Tests: test/sim/feedback1-tank-triggers.test.js (the data, kit = data, a real battle on 战场#01(下半) in which each of
  the six casts with an enemy in range before any hit and each of the other 46 MANUAL 重装 records casts on the first
  hit); data.test.js trigger expectations; the 灰毫 / 号角 kit tests follow; docs-consistency.
- Docs: DESIGN §21.29 (new), §5.6, §19.2, §20.2, §21.20 (settled row), §21.23; DATA, SIM, PLAYING, CHANGELOG 0.1.1;
  research 03 §1.4 drops the arknights.wiki.gg "offensive / defensive" summary and records the activity_table history
  (上半 had no TANK row, 下半 added it) and this deviation; research 00-INDEX.
This commit is contained in:
sganggs committed 2026-10-03 19:13:35 +08:00
1 parent e6ab4e1ff8
commit 2dba6da598
20 files changed
+325 -58

No files matched your search

+41 -5
View File
@@ -425,6 +425,23 @@ const TRIGGER_RENAME = { ALWAYS: 'SP_FULL', CUSTOM_RANGE_SEARCH_ENEMY: 'CUSTOM_R
*/
const ATTACK_RANGE_CHANGE = /攻击(?:范围|距离)(?:与溅射范围)?(?:扩大|改变|缩小|缩短|加长|增加|\+)/;
/**
* A deliberate deviation from the official 技能策略 (DESIGN §21.29): the owner's decision of 2026-10-03 after community
* feedback ("反馈的人太多了"; GitHub issue #4, PR #12). The 下半 data's TANK class row (`TANK | | | 0 | TAKE_DAMAGE`,
* added with 下半 — 上半 had no TANK row) makes every MANUAL 重装 skill wait for a hit; these six offensive skills cast
* with an enemy in range instead — the basic strategy, DEFAULT. Keyed by the NORMAL chess id (the elite follows through
* its baseId) and the skill id, never by the skill id alone: 灰毫 S1 skcom_atk_up[3] is a generic skill other chess
* carry too (惊蛰, 幽灵鲨, 耶拉, 莫斯提马, 莱恩哈特). The record keeps the official row in `rawRule` (TAKE_DAMAGE) for
* traceability; validateAll fails the build when an entry no longer meets a TAKE_DAMAGE row or its skill. 深巡 S1
* 侵袭破坏应对 keeps TAKE_DAMAGE.
*/
const TRIGGER_DEVIATIONS = Object.freeze({
chess_char_1_04_a: { skchr_udflow_2: 'DEFAULT' }, // 深巡 S2 行动能力剥夺
chess_char_1_20_a: { skchr_liskam_2: 'DEFAULT' }, // 雷蛇 S2 反击电弧
chess_char_2_18_a: { 'skcom_atk_up[3]': 'DEFAULT', skchr_ashlok_2: 'DEFAULT' }, // 灰毫 S1 攻击力强化·γ型, S2 专注轰击
chess_char_5_08_a: { skchr_horn_2: 'DEFAULT', skchr_horn_3: 'DEFAULT' }, // 号角 S2 暴风号令, S3 终极防线
});
/**
* Resolve the auto-cast rule of a skill record (PRTS 卫戍协议/帮助 §作战阶段 技能操作 — the official skill strategies;
* DESIGN §5.6):
@@ -437,11 +454,14 @@ const ATTACK_RANGE_CHANGE = /攻击(?:范围|距离)(?:与溅射范围)?(?:扩
* an AUTO skill fires by its own rule (PRTS 古米 S1 备注: "此技能在存在生命值不满的可治疗角色时可触发");
* - else, for an operator's MANUAL skill with a 技能范围 (a rangeId that is not an attack-range change): SKILL_RANGE,
* "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能", customRangeGrid = the skill range;
* - else DEFAULT (the basic strategy: ready + about to attack / heal).
* - 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.
* @param {object} skill record from buildSkill (skillId, skillType, desc, rangeGrid)
* @param {{operator?: boolean}} opts operator = a chess (the 技能范围 strategy is written for 干员; summons keep DEFAULT)
* @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)
*/
function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false } = {}) {
function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false, chessId = null } = {}) {
const rows = Object.values(ctx.ac.skillTriggerDataList || {});
const manual = skill.skillType === 'MANUAL';
const pick =
@@ -453,6 +473,8 @@ function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false }
return { rule: 'SKILL_RANGE', rawRule: 'DEFAULT', customRangeGrid: skill.rangeGrid.map((p) => p.slice()) };
}
const rawRule = pick ? pick.skillTriggerType : 'DEFAULT';
const deviation = chessId ? TRIGGER_DEVIATIONS[chessId]?.[skill.skillId] : null;
if (deviation) return { rule: deviation, rawRule, customRangeGrid: null };
const rule = TRIGGER_RENAME[rawRule] || rawRule;
let customRangeGrid = null;
if (rawRule === 'CUSTOM_RANGE_SEARCH_ENEMY') {
@@ -808,7 +830,8 @@ function buildChess(ctx) {
Object.assign(rec, traitDefault.classify);
// Every skill unlocked at the chess status (DESIGN §16), at the chess skill level; the trigger is
// resolved per skill (resolveTrigger: charId rows by index, class rows for every MANUAL skill, 技能范围).
// resolved per skill (resolveTrigger: charId rows by index, class rows for every MANUAL skill, 技能范围, and the
// deliberate deviations of TRIGGER_DEVIATIONS keyed by the normal chess id).
const sIdx = shop.defaultSkillIndex ?? 0;
const sEntry = char.skills?.[sIdx];
const skillLevel = status.skillLevel || 1;
@@ -817,7 +840,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 });
s.trigger = resolveTrigger(ctx, char, shop.charId, i, s, { operator: true, chessId: baseId });
s.index = i;
s.overrideTokenKey = se.overrideTokenKey || null;
skillRecs.push(s);
@@ -3068,6 +3091,19 @@ function validateAll(f) {
if (!Array.isArray(c.skills) || c.skills.filter((s) => s.isDefault).length !== 1 || c.skills.find((s) => s.isDefault)?.skillId !== c.skill?.skillId) err(`chess ${c.chessId}: skills[] without exactly one default = skill`);
if (c.modules && (c.modules.filter((m) => m.isDefault).length !== (c.module?.active ? 1 : 0) || !c.statsBase || !c.traitBase || !c.talentsBase)) err(`chess ${c.chessId}: inconsistent module choices`);
}
// the deliberate trigger deviations (DESIGN §21.29) still override an official TAKE_DAMAGE row, on the normal chess
// and its elite alike
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}`);
for (const [skillId, rule] of Object.entries(skillsOf)) {
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}`);
}
}
}
for (const b of Object.values(bonds)) {
for (const m of b.members) if (!chess[m]) err(`bond ${b.bondId}: member ${m} missing`);
if (!b.thresholds.length) err(`bond ${b.bondId}: no thresholds`);