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:
1 parent
e6ab4e1ff8
commit
2dba6da598
20 files changed
+325
-58
No files matched your search
+2
-1
@@ -8,6 +8,7 @@
|
||||
|
||||
- 阵法术师(卡涅利安、薄绿、蜜蜡、圣聆初雪)开技能后每次攻击同时打到攻击范围内所有敌人,轰击术师(阿罗玛、协律)打整条直线上的敌人;以前只打一个目标再溅射。
|
||||
- 卡涅利安蓄力开一技能时按官方不攻击,只保留高防御、高法抗。
|
||||
- 深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能改为攻击范围内有敌人时就释放,不用等受到攻击(反馈的人很多,这是有意与官方「重装干员受到伤害时释放技能」不同的调整;GitHub issue #4、PR #12)。深巡一技能和其他重装干员的手动技能照旧在受到伤害时释放。
|
||||
- 野鬃二技能沿她的朝向推开敌人,不再把身后的敌人推向保护目标点。
|
||||
- 琳琅诗怀雅三技能的金币从她身边向外推开,落在她前方 2–4 格内的敌人和她阻挡的敌人身上。
|
||||
- 飞行敌人按官方是「静态刚体」:能被打到,但推拉不动(薄绿、锏、崖心、歌蕾蒂娅等)。
|
||||
@@ -93,7 +94,7 @@
|
||||
- 源石溶剂两个白色合成金色后又多给一个白色:所有装备反复测试都没有复现。
|
||||
- 医疗挡住会走动的领袖后不奶人、改打领袖:没有复现,医疗挡住敌人时照常治疗队友(与官方一致)。看到的大伤害来自其他干员:卢西恩被挡住后不再闪避。
|
||||
- 休露丝的优惠被免费拿到的谢拉格干员用掉:没有复现,晋升奖励、寻呼模块、拟态物质等免费获得不会用掉,只有在商店购买才会。
|
||||
- 深巡放在围栏里不攻击:没有复现,她照常攻击攻击范围内的敌人;看起来不打,多半是攻击范围朝着围栏或阻隔工事,或者没人打她所以技能不放。
|
||||
- 深巡放在围栏里不攻击:没有复现,她照常攻击攻击范围内的敌人;看起来不打,多半是攻击范围朝着围栏或阻隔工事,或者没人打她所以技能不放(她的二技能现在攻击范围内有敌人就会释放,见「战斗」)。
|
||||
- 另外三条只有一部分属实:烛煌三技能在战斗里一直按新范围攻击,错的只是详情卡的范围小图;奥术盟约的效果一直生效,看起来不生效是因为标准模拟禁用了它;坚固维式重锤的锁血一直有效,看起来没生效是因为以前每场战斗只锁一次,站在阿戈尔干员身前被开场吞噬时也会提前用掉。
|
||||
|
||||
## 0.1.0 — 2026-10-02
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"chess_char_1_01_a":{"chessId":"chess_char_1_01_a","baseId":"chess_char_1_01_a","goldenId":"chess_char_1_01_b","isGolden":false,"tier":1,"identifier":0,"isHidden":false,"isDiy":false,"visible":true,"chessType":"PRESET","shopSortId":1,"charId":"char_498_inside","name":"隐现","appellation":"Insider","rarity":5,"profession":"SNIPER","subProfessionId":"fastshot","subProfessionName":"速射手","position":"RANGED","nationId":"laterano","bonds":["lateranoShip","swiftShip"],"garrisonIds":["garrison_16_a"],"price":2,"sellPrice":1,"upgradeNum":3,"upgradeChessId":"chess_char_1_01_b","status":{"phase":1,"level":55,"skillLevel":4,"equipLevel":0},"stats":{"maxHp":1123,"atk":399,"def":140,"res":0,"cost":15,"blockCnt":1,"bat":1,"aspd":100,"respawnTime":80,"spRecovery":1,"hpRecoveryPerSec":0,"moveSpeed":1,"tauntLevel":0,"massLevel":0,"deployLimit":1,"deckStack":0},"immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"rangeId":"3-3","rangeGrid":[[1,0],[1,1],[1,2],[1,3],[0,0],[0,1],[0,2],[0,3],[-1,0],[-1,1],[-1,2],[-1,3]],"dmgType":"phys","attackKind":"ranged","projectile":"arrow","canHitFly":true,"targetPriority":"fly","trait":{"desc":"优先攻击空中单位","descRaw":"优先攻击空中单位","bb":{},"bbStr":{},"rangeGrid":null},"skill":{"skillId":"skchr_inside_2","iconId":"skchr_inside_2","name":"“解决麻烦”","level":4,"desc":"攻击力+80%,攻击间隔略微缩短,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n攻击装有14发弹药,打完后结束(可随时停止技能)","descRaw":"攻击力<@ba.vup>+80%</>,攻击间隔<@ba.vup>略微缩短</>,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n<@ba.rem>攻击装有14发弹药,打完后结束(可随时停止技能)</>","skillType":"MANUAL","durationType":"AMMO","duration":0,"spType":"INCREASE_WITH_TIME","spCost":24,"initSp":8,"maxChargeTime":1,"increment":1,"bb":{"atk":0.8,"base_attack_time":-0.3,"attack@trigger_time":14},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skchr_inside_2","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null},"talents":[{"index":0,"name":"火力支援","desc":"在场停留20秒后,自身弹药类技能弹药上限+2","descRaw":"在场停留20秒后,自身弹药类技能弹药上限+2","bb":{"self_ammo":2,"duration":20},"bbStr":{},"rangeGrid":null,"tokenKey":null,"hidden":false,"fromModule":false}],"tokens":[],"module":{"id":"uniequip_002_inside","name":"“最初的惊喜”","type":"MAR-X","level":0,"active":false},"assets":{"avatar":"char_498_inside","portrait":"char_498_inside_1","spine":"char_498_inside","skillIcon":"skchr_inside_2","subProfIcon":"sub_fastshot_icon"},"skills":[{"skillId":"skchr_inside_1","iconId":"skchr_inside_1","name":"“不惹麻烦”","level":4,"desc":"每次攻击造成相当于攻击力185%的物理伤害\n攻击装有4发弹药,打完后结束(可随时停止技能)","descRaw":"每次攻击造成相当于攻击力<@ba.vup>185%</>的物理伤害\n<@ba.rem>攻击装有4发弹药,打完后结束(可随时停止技能)</>","skillType":"AUTO","durationType":"AMMO","duration":0,"spType":"INCREASE_WHEN_ATTACK","spCost":14,"initSp":0,"maxChargeTime":1,"increment":1,"bb":{"attack@atk_scale":1.85,"attack@trigger_time":4},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skchr_inside_1","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":0,"overrideTokenKey":null,"isDefault":false},{"skillId":"skchr_inside_2","iconId":"skchr_inside_2","name":"“解决麻烦”","level":4,"desc":"攻击力+80%,攻击间隔略微缩短,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n攻击装有14发弹药,打完后结束(可随时停止技能)","descRaw":"攻击力<@ba.vup>+80%</>,攻击间隔<@ba.vup>略微缩短</>,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n<@ba.rem>攻击装有14发弹药,打完后结束(可随时停止技能)</>","skillType":"MANUAL","durationType":"AMMO","duration":0,"spType":"INCREASE_WITH_TIME","spCost":24,"initSp":8,"maxChargeTime":1,"increment":1,"bb":{"atk":0.8,"base_attack_time":-0.3,"attack@trigger_time":14},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skchr_inside_2","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null,"isDefault":true}]},"chess_char_1_01_b":{"chessId":"chess_char_1_01_b","baseId":"chess_char_1_01_a","goldenId":"chess_char_1_01_b","isGolden":true,"tier":1,"identifier":133,"isHidden":false,"isDiy":false,"visible":true,"chessType":"PRESET","shopSortId":1,"charId":"char_498_inside","name":"隐现","appellation":"Insider","rarity":5,"profession":"SNIPER","subProfessionId":"fastshot","subProfessionName":"速射手","position":"RANGED","nationId":"laterano","bonds":["lateranoShip","swiftShip"],"garriLine truncated
|
||||
{"chess_char_1_01_a":{"chessId":"chess_char_1_01_a","baseId":"chess_char_1_01_a","goldenId":"chess_char_1_01_b","isGolden":false,"tier":1,"identifier":0,"isHidden":false,"isDiy":false,"visible":true,"chessType":"PRESET","shopSortId":1,"charId":"char_498_inside","name":"隐现","appellation":"Insider","rarity":5,"profession":"SNIPER","subProfessionId":"fastshot","subProfessionName":"速射手","position":"RANGED","nationId":"laterano","bonds":["lateranoShip","swiftShip"],"garrisonIds":["garrison_16_a"],"price":2,"sellPrice":1,"upgradeNum":3,"upgradeChessId":"chess_char_1_01_b","status":{"phase":1,"level":55,"skillLevel":4,"equipLevel":0},"stats":{"maxHp":1123,"atk":399,"def":140,"res":0,"cost":15,"blockCnt":1,"bat":1,"aspd":100,"respawnTime":80,"spRecovery":1,"hpRecoveryPerSec":0,"moveSpeed":1,"tauntLevel":0,"massLevel":0,"deployLimit":1,"deckStack":0},"immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"rangeId":"3-3","rangeGrid":[[1,0],[1,1],[1,2],[1,3],[0,0],[0,1],[0,2],[0,3],[-1,0],[-1,1],[-1,2],[-1,3]],"dmgType":"phys","attackKind":"ranged","projectile":"arrow","canHitFly":true,"targetPriority":"fly","trait":{"desc":"优先攻击空中单位","descRaw":"优先攻击空中单位","bb":{},"bbStr":{},"rangeGrid":null},"skill":{"skillId":"skchr_inside_2","iconId":"skchr_inside_2","name":"“解决麻烦”","level":4,"desc":"攻击力+80%,攻击间隔略微缩短,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n攻击装有14发弹药,打完后结束(可随时停止技能)","descRaw":"攻击力<@ba.vup>+80%</>,攻击间隔<@ba.vup>略微缩短</>,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n<@ba.rem>攻击装有14发弹药,打完后结束(可随时停止技能)</>","skillType":"MANUAL","durationType":"AMMO","duration":0,"spType":"INCREASE_WITH_TIME","spCost":24,"initSp":8,"maxChargeTime":1,"increment":1,"bb":{"atk":0.8,"base_attack_time":-0.3,"attack@trigger_time":14},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skchr_inside_2","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null},"talents":[{"index":0,"name":"火力支援","desc":"在场停留20秒后,自身弹药类技能弹药上限+2","descRaw":"在场停留20秒后,自身弹药类技能弹药上限+2","bb":{"self_ammo":2,"duration":20},"bbStr":{},"rangeGrid":null,"tokenKey":null,"hidden":false,"fromModule":false}],"tokens":[],"module":{"id":"uniequip_002_inside","name":"“最初的惊喜”","type":"MAR-X","level":0,"active":false},"assets":{"avatar":"char_498_inside","portrait":"char_498_inside_1","spine":"char_498_inside","skillIcon":"skchr_inside_2","subProfIcon":"sub_fastshot_icon"},"skills":[{"skillId":"skchr_inside_1","iconId":"skchr_inside_1","name":"“不惹麻烦”","level":4,"desc":"每次攻击造成相当于攻击力185%的物理伤害\n攻击装有4发弹药,打完后结束(可随时停止技能)","descRaw":"每次攻击造成相当于攻击力<@ba.vup>185%</>的物理伤害\n<@ba.rem>攻击装有4发弹药,打完后结束(可随时停止技能)</>","skillType":"AUTO","durationType":"AMMO","duration":0,"spType":"INCREASE_WHEN_ATTACK","spCost":14,"initSp":0,"maxChargeTime":1,"increment":1,"bb":{"attack@atk_scale":1.85,"attack@trigger_time":4},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skchr_inside_1","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":0,"overrideTokenKey":null,"isDefault":false},{"skillId":"skchr_inside_2","iconId":"skchr_inside_2","name":"“解决麻烦”","level":4,"desc":"攻击力+80%,攻击间隔略微缩短,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n攻击装有14发弹药,打完后结束(可随时停止技能)","descRaw":"攻击力<@ba.vup>+80%</>,攻击间隔<@ba.vup>略微缩短</>,优先攻击使用远程武器的敌人并且不容易成为敌人的攻击目标\n<@ba.rem>攻击装有14发弹药,打完后结束(可随时停止技能)</>","skillType":"MANUAL","durationType":"AMMO","duration":0,"spType":"INCREASE_WITH_TIME","spCost":24,"initSp":8,"maxChargeTime":1,"increment":1,"bb":{"atk":0.8,"base_attack_time":-0.3,"attack@trigger_time":14},"bbStr":{},"rangeId":null,"rangeGrid":null,"prefabId":"skchr_inside_2","trigger":{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null},"index":1,"overrideTokenKey":null,"isDefault":true}]},"chess_char_1_01_b":{"chessId":"chess_char_1_01_b","baseId":"chess_char_1_01_a","goldenId":"chess_char_1_01_b","isGolden":true,"tier":1,"identifier":133,"isHidden":false,"isDiy":false,"visible":true,"chessType":"PRESET","shopSortId":1,"charId":"char_498_inside","name":"隐现","appellation":"Insider","rarity":5,"profession":"SNIPER","subProfessionId":"fastshot","subProfessionName":"速射手","position":"RANGED","nationId":"laterano","bonds":["lateranoShip","swiftShip"],"garriLine truncated
|
||||
+2
-2
@@ -188,7 +188,7 @@ Top level: `{ season, seasonName, modes, economy, lpCapPerRound, bossOvertimeAft
|
||||
| `bb`, `bbStr` | `{"atk":1,"base_attack_time":-0.3,"attack@trigger_time":14}` | kit input (DESIGN §5.6) |
|
||||
| `rangeId`, `rangeGrid` | `null` | skill range override |
|
||||
| `prefabId`, `overrideTokenKey` | | |
|
||||
| `trigger` | `{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null}` | auto-cast rule (the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作; §2.2). `rule` ∈ `DEFAULT` (basic strategy), `SKILL_RANGE` (a MANUAL skill with a 技能范围 of its own: `customRangeGrid` = its `rangeGrid`, `rawRule` `DEFAULT` — no official row), `TAKE_DAMAGE` (every MANUAL 重装 skill), `SP_FULL` (official `ALWAYS`: 执旗手 / 战术家 / 吟游者 MANUAL skills), `CUSTOM_RANGE` (official `CUSTOM_RANGE_SEARCH_ENEMY`, uses `customRangeGrid`), `SEARCH` (解放者 / 阵法术师 MANUAL skills, 安洁莉娜 S2/S3), `MLYSS_WTRMAN` (缪尔赛思), `GDGLOW_SKILL_2` (荒芜拉普兰德, 纯烬艾雅法拉 S3: "全场存在可选目标时释放技能"). |
|
||||
| `trigger` | `{"rule":"DEFAULT","rawRule":"DEFAULT","customRangeGrid":null}` | auto-cast rule (the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作; §2.2). `rule` ∈ `DEFAULT` (basic strategy), `SKILL_RANGE` (a MANUAL skill with a 技能范围 of its own: `customRangeGrid` = its `rangeGrid`, `rawRule` `DEFAULT` — no official row), `TAKE_DAMAGE` (every MANUAL 重装 skill but six: 深巡 / 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 are `DEFAULT` with `rawRule` `TAKE_DAMAGE` — a deliberate deviation, `tools/build-data.mjs TRIGGER_DEVIATIONS`, DESIGN §21.29), `SP_FULL` (official `ALWAYS`: 执旗手 / 战术家 / 吟游者 MANUAL skills), `CUSTOM_RANGE` (official `CUSTOM_RANGE_SEARCH_ENEMY`, uses `customRangeGrid`), `SEARCH` (解放者 / 阵法术师 MANUAL skills, 安洁莉娜 S2/S3), `MLYSS_WTRMAN` (缪尔赛思), `GDGLOW_SKILL_2` (荒芜拉普兰德, 纯烬艾雅法拉 S3: "全场存在可选目标时释放技能"). |
|
||||
|
||||
### 2.1 Combat classification heuristic (`dmgType` / `attackKind` / `projectile` / `canHitFly`)
|
||||
- `dmgType`: MEDIC (except `incantationmedic`) and `bard` → `heal`; trait text containing 法术伤害 or profession CASTER → `arts`; else `phys`.
|
||||
@@ -492,7 +492,7 @@ Glyph legend (`rows`):
|
||||
5. **Every non-DIY chess (258 records: 129 normal + 129 golden) has a resolvable default skill, stats and range** — no
|
||||
skill anomalies.
|
||||
6. **Skill triggers** (§2.2): the class rows cover every MANUAL skill of the class (all 重装 MANUAL skills are
|
||||
`TAKE_DAMAGE`; 薄绿 / 卡涅利安 / 蜜蜡 / 玛恩纳 S2 `SEARCH`; 伺夜 / 魔王 / 浊心斯卡蒂 S3 `SP_FULL`) and no AUTO skill (古米 /
|
||||
`TAKE_DAMAGE` but the six of the deliberate deviation, DESIGN §21.29, which are `DEFAULT` with `rawRule` `TAKE_DAMAGE`; 薄绿 / 卡涅利安 / 蜜蜡 / 玛恩纳 S2 `SEARCH`; 伺夜 / 魔王 / 浊心斯卡蒂 S3 `SP_FULL`) and no AUTO skill (古米 /
|
||||
雷蛇 / 瑕光 / 塞雷娅 / 号角 / 信仰搅拌机 S1, 伺夜 S1/S2, 魔王 S1); 13 MANUAL skills (26 normal + elite records) with
|
||||
their own 技能范围 are `SKILL_RANGE` (德克萨斯 S2, 凛御银灰 S2, 锏 S2/S3, 异客 S3, 忍冬 S2, 焰尾 S2 …).
|
||||
7. **Trait candidate `rangeId`** (送葬人, 松果 1-3; 风丸, 归溟幽灵鲨 x-4) is the trait-effect area, exposed as
|
||||
|
||||
+18
-5
@@ -280,7 +280,7 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
|
||||
|
||||
SP: `spType` `INCREASE_WITH_TIME` (+`spRecovery`/s), `INCREASE_WHEN_ATTACK` (+1 per attack), `INCREASE_WHEN_TAKEN_DAMAGE` (+1 per hit taken); starts at `initSp`; no SP gain while a duration skill is active. `maxChargeTime > 1` ⇒ charges.
|
||||
|
||||
Auto-trigger rules (`skillTrigger.rule` in data — the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作; §20.2): `DEFAULT` (SP ready AND about to attack/heal AND an enemy/injured ally inside the **initial** range, or an enemy the unit blocks; with `trigger.allies` + `hpAtMost` an ally of the skill grid at or below that HP ratio — 塞雷娅 S1 急救, whose cast replaces that attack and is withdrawn with its charge if the ally recovers first), `SKILL_RANGE` (a MANUAL skill with a 技能范围 of its own, not an attack-range change: any living enemy, stealthed ones included, on the skill range, checked every tick — "仅在技能范围内存在敌人(无视其不可选中)时释放技能"; 13 skills / 26 records), `TAKE_DAMAGE` (on taking damage — any damage, 无来源 included, never a 流失; every MANUAL 重装 skill, §21.12), `SP_FULL` (immediately — 执旗手 / 战术家 / 吟游者 MANUAL skills, and AUTO DP skills such as 伺夜 S1, 焰尾 S1, 凛御银灰 S1), `CUSTOM_RANGE` (enemy inside the custom grid), `SEARCH` (an enemy inside the initial range, checked every tick — no attack needed; 解放者 / 阵法术师 MANUAL skills), `GDGLOW_SKILL_2` (a valid target anywhere on the field: 荒芜拉普兰德, 纯烬艾雅法拉 S3), special ids. The class rows cover **every MANUAL skill** of the class and never an AUTO skill (AUTO skills keep their own rule — an inference: PRTS names whole classes; the data's `skillIndex 0` / `-1` sentinel argues the other way, §20.2). **Operation cooldown**: the engine auto-casts a MANUAL skill no sooner than `AUTO_OP_COOLDOWN` (3 s) after its previous cast and after the unit's battle-start deployment ("自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却"; `Battle._deploy` initial, `battle.flags.startOpCooldown`); kits with an automatic cast of their own check `skill.opCooling`; a pending "next attack" never takes a second charge. PASSIVE skills are always on.
|
||||
Auto-trigger rules (`skillTrigger.rule` in data — the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作; §20.2): `DEFAULT` (SP ready AND about to attack/heal AND an enemy/injured ally inside the **initial** range, or an enemy the unit blocks; with `trigger.allies` + `hpAtMost` an ally of the skill grid at or below that HP ratio — 塞雷娅 S1 急救, whose cast replaces that attack and is withdrawn with its charge if the ally recovers first), `SKILL_RANGE` (a MANUAL skill with a 技能范围 of its own, not an attack-range change: any living enemy, stealthed ones included, on the skill range, checked every tick — "仅在技能范围内存在敌人(无视其不可选中)时释放技能"; 13 skills / 26 records), `TAKE_DAMAGE` (on taking damage — any damage, 无来源 included, never a 流失; every MANUAL 重装 skill, §21.12 — but the six of the deliberate deviation, §21.29: 深巡 / 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 are `DEFAULT`, `rawRule` keeping the official TAKE_DAMAGE), `SP_FULL` (immediately — 执旗手 / 战术家 / 吟游者 MANUAL skills, and AUTO DP skills such as 伺夜 S1, 焰尾 S1, 凛御银灰 S1), `CUSTOM_RANGE` (enemy inside the custom grid), `SEARCH` (an enemy inside the initial range, checked every tick — no attack needed; 解放者 / 阵法术师 MANUAL skills), `GDGLOW_SKILL_2` (a valid target anywhere on the field: 荒芜拉普兰德, 纯烬艾雅法拉 S3), special ids. The class rows cover **every MANUAL skill** of the class and never an AUTO skill (AUTO skills keep their own rule — an inference: PRTS names whole classes; the data's `skillIndex 0` / `-1` sentinel argues the other way, §20.2). **Operation cooldown**: the engine auto-casts a MANUAL skill no sooner than `AUTO_OP_COOLDOWN` (3 s) after its previous cast and after the unit's battle-start deployment ("自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却"; `Battle._deploy` initial, `battle.flags.startOpCooldown`); kits with an automatic cast of their own check `skill.opCooling`; a pending "next attack" never takes a second charge. PASSIVE skills are always on.
|
||||
|
||||
**SkillSpec** (authored per chess in `sim/content/kits/tierN.js`; values come from the chess's skill blackboard at its level — normal Lv4, elite Lv7 — so a spec is a *function of the blackboard*):
|
||||
```js
|
||||
@@ -711,7 +711,7 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
|
||||
- **Now** (§5.5 Blocking): every tick, moving or not, an unblocked blockable enemy is blocked by the nearest operator in contact that has free capacity for its weight — the front operator takes over when the blocker dies, is withdrawn or is stunned (`stun` releases its enemies); an operator whose capacity frees up grabs an overlapping enemy; an enemy that finds no room walks on. A head-on enemy stops at contact, ≈ 0.71 tile from the blocker's centre, outside its tile (the small official deceleration is not modelled). A knocked-out or undeployed operator never blocks.
|
||||
- **Consequences, both official**: "可以选择且优先选择阻挡单位" (PRTS 选择器: the selector of units whose range is smaller than their block radius) — a **melee** blocker (data `position` MELEE: 要塞 / 领主 / 哨戒铁卫 included; every blocker since §20.3) may always target the enemies it blocks, in range or not, and targets them first (`Battle.blockedTargets` → `ai.js acquireTargets`, `targeting.js sortEnemyTargets`, the DEFAULT skill trigger — PRTS 卫戍协议/帮助 "敌人被近战干员自身阻挡"); 瑕光 S2 also sleeps the enemy she blocks (PRTS 备注). A rear operator whose range ends at the blocker's tile no longer reaches the enemy held in front of it (a regression sweep of 143 blocker × skill pairs: own-tile defenders 0.96–1.05×).
|
||||
- **Ranged operators on melee tiles** (卫戍协议 "所有行动内远程干员可部署在近战位") — **superseded by §20.3**: the integration QA had made them target by their range alone (PRTS 索敌的概念 "我方索敌优先级:阻挡(近战限定)"; "远程位干员通常而言不会优先攻击自己阻挡的目标;甚至如果这个目标被阻挡在该干员的身后…无法攻击到这个敌人"): no blocked-first priority, no out-of-range target, no skill cast for the enemy they held. The user's rule after playtest #6 — "阻挡了就一定要能打到" — makes every blocker hit what it blocks.
|
||||
- 灰毫 S2 专注轰击 — **superseded by §20.2**: v2.4 read the 下半 重装 special strategy "受到伤害时释放技能" as skill 1 only (BWIKI "重装职业干员 技能1,受到伤害后自动释放"; the data row has `skillIndex 0`, with `-1` as the all-skills sentinel — research 03 C1) and left S2 DEFAULT. The class rows now cover every MANUAL skill of the class, so 灰毫 S2 is TAKE_DAMAGE (the counter-evidence stays recorded in §20.2).
|
||||
- 灰毫 S2 专注轰击 — **superseded by §20.2**: v2.4 read the 下半 重装 special strategy "受到伤害时释放技能" as skill 1 only (BWIKI "重装职业干员 技能1,受到伤害后自动释放"; the data row has `skillIndex 0`, with `-1` as the all-skills sentinel — research 03 C1) and left S2 DEFAULT. The class rows now cover every MANUAL skill of the class, so 灰毫 S2 is TAKE_DAMAGE (the counter-evidence stays recorded in §20.2) — and DEFAULT again since §21.29, a deliberate deviation the owner decided (with 灰毫 S1).
|
||||
- [ASSUMED] the nearest blocker wins, ties by scan order.
|
||||
- Tests: `test/sim/playtest5_blocking.test.js` (16, incl. the user's case on act2 m01 with real operators, the ranged blocker and 银灰 as a melee-position ranged attacker), adjusted geometry in `kits_alt_t5` / `engine-requests` / `enemies_bosses`.
|
||||
|
||||
@@ -848,7 +848,7 @@ Eight workstreams were merged into `playtest6` (WH → WG → WD → WC → WB
|
||||
|
||||
- **#15 seen**: a skill whose range is wider than the attack range did not fire when an enemy entered it (德克萨斯 剑雨 13 tiles vs 2; 凛御银灰 S2; 锏 S3). **Cause**: every MANUAL skill used the basic rule, and research 03 C1 read the class rows (`skillIndex 0`) as "skill 1 only" (BWIKI).
|
||||
- **Official** (PRTS 卫戍协议/帮助 §作战阶段 技能操作): basic rule "技能就绪,且即将进行普通攻击/治疗"; a skill with its own 技能范围 (not an attack-range increase) "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能"; class strategies — 重装 "不受技能范围影响,受到伤害时释放技能" (下半, 2026/3/14), 战术家 / 执旗手 / 吟游者 when ready, 解放者 / 阵法术师 an enemy in the initial range, 澄闪 / 纯烬 / 荒芜 S3 "全场存在可选目标时"; "自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却".
|
||||
- **Now** (§5.6): the class rows cover **every MANUAL skill** of the class and never an AUTO one — an inference (PRTS names whole classes; under the old reading the 阵法术师 row could never cover 薄绿's default S2, a phalanx never attacks with its skill off); counter-evidence kept: BWIKI's "技能1" and the data's `skillIndex 0` / `-1` sentinel. `SKILL_RANGE`: 13 skills (26 records with the elites), any living enemy on the skill range, checked every tick (attack-range changes keep the basic rule). `GDGLOW_SKILL_2`: a valid target anywhere. The 3 s operation cooldown after each automatic cast and after the battle-start deployment (`flags.startOpCooldown`; the test harness sets it to 0); kit-driven casts of MANUAL skills (波登可 S1, 雪猎 bullets, the 流形 copy) check `skill.opCooling`; AUTO skills are exempt; a pending "next attack" never takes a second charge. Players notice: every 重装 manual skill (号角 S3, 瑕光 S3, 灰毫 S2 — reversing §19.2 —, 古米 S2 …) now waits for the operator to be hit.
|
||||
- **Now** (§5.6): the class rows cover **every MANUAL skill** of the class and never an AUTO one — an inference (PRTS names whole classes; under the old reading the 阵法术师 row could never cover 薄绿's default S2, a phalanx never attacks with its skill off); counter-evidence kept: BWIKI's "技能1" and the data's `skillIndex 0` / `-1` sentinel. `SKILL_RANGE`: 13 skills (26 records with the elites), any living enemy on the skill range, checked every tick (attack-range changes keep the basic rule). `GDGLOW_SKILL_2`: a valid target anywhere. The 3 s operation cooldown after each automatic cast and after the battle-start deployment (`flags.startOpCooldown`; the test harness sets it to 0); kit-driven casts of MANUAL skills (波登可 S1, 雪猎 bullets, the 流形 copy) check `skill.opCooling`; AUTO skills are exempt; a pending "next attack" never takes a second charge. Players notice: every 重装 manual skill (号角 S3, 瑕光 S3, 灰毫 S2 — reversing §19.2 —, 古米 S2 …) now waits for the operator to be hit — **partly reverted by §21.29**: by the owner's deliberate deviation (2026-10-03, community feedback) 深巡 / 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 cast with an enemy in range (DEFAULT) again.
|
||||
- **Integration follow-up (WE review round 2)**: the AUTO DP skills 焰尾 S1 迅敏直觉 and 凛御银灰 S1 周旋的谋略 ("立即获得…部署费用") are `SP_FULL` like 伺夜 S1 and 德克萨斯's 冲锋号令·γ型 — an AUTO skill takes no 技能策略 (they used to wait 12–16 s for the next attack).
|
||||
- **#16**: 古米 备用军粮 (AUTO) had picked up the 重装 row. PRTS 备注 "存在生命值不满的可治疗角色时可触发;…切换至治疗模式…直至古米完成一次普通攻击的治疗": it fires on an injured ally of its x-4 area and her next attack heals the most injured ally there (`heal_scale` × ATK); S2 食粮烹制 is MANUAL, so the 重装 rule. Audit: 塞雷娅 S1 急救 — PRTS 备注 "仅在周围有符合血量条件的友方单位时可触发,触发时会替换当次攻击", 修正 "血量小于等于一半" — is checked at her attack (DEFAULT + `allies` + `hpAtMost` 0.5) and that attack becomes the heal; a cast whose ally recovered first is withdrawn with its charge.
|
||||
- **#8**: 新约能天使 S2 gave the shield to the fastest attacker; official (PRTS 备注) "攻击范围内仇恨值最高的我方干员" — now `aggroCmp` (taunt, then the latest deployed), through `battle.alliesInGrid` (孤立 units skipped). The amounts were already right: 180 % (精锐 200 %) of each holder's own max HP, decaying by initial / 30 per s.
|
||||
@@ -859,7 +859,7 @@ Eight workstreams were merged into `playtest6` (WH → WG → WD → WC → WB
|
||||
| Skill (examples) | type | before → after | why |
|
||||
|---|---|---|---|
|
||||
| 德克萨斯 S2 剑雨, 凛御银灰 S2, 锏 S2 / S3, 异客 S3, 焰尾 S2, 忍冬 S2, 诗怀雅 S1, 山 S3, 小满 S2, 归溟幽灵鲨 S1, 崖心 S2, 见行者 S2 | MANUAL | DEFAULT (kit CUSTOM_RANGE) → SKILL_RANGE | own 技能范围 |
|
||||
| 深巡 S2, 古米 S2, 折桠 S2, 瑕光 S2 / S3, 信仰搅拌机 S2 / S3, 泥岩 S3, 塞雷娅 S3, 余 S2, 角峰 / 雷蛇 / 泡泡 / 菲莱 / 蛇屠箱 S2, 灰毫 S2, 星熊 S3, 号角 S2 / S3 | MANUAL | DEFAULT → TAKE_DAMAGE | 重装 row |
|
||||
| 深巡 S2, 古米 S2, 折桠 S2, 瑕光 S2 / S3, 信仰搅拌机 S2 / S3, 泥岩 S3, 塞雷娅 S3, 余 S2, 角峰 / 雷蛇 / 泡泡 / 菲莱 / 蛇屠箱 S2, 灰毫 S2, 星熊 S3, 号角 S2 / S3 | MANUAL | DEFAULT → TAKE_DAMAGE (深巡 / 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2 back to DEFAULT, §21.29) | 重装 row (§21.29: a deliberate deviation for six skills) |
|
||||
| 薄绿 S2, 蜜蜡 S2, 卡涅利安 S2 / S3, 玛恩纳 S2 | MANUAL | DEFAULT → SEARCH | 阵法术师 / 解放者 row |
|
||||
| 伺夜 S3, 魔王 S2 / S3, 浊心斯卡蒂 S3 | MANUAL | DEFAULT → SP_FULL | 战术家 / 吟游者 row |
|
||||
| 古米 S1, 瑕光 S1, 塞雷娅 S1 | AUTO | TAKE_DAMAGE → own rule (古米: injured ally; 塞雷娅: at her attack, ally ≤ 50 %) | AUTO: no 技能策略 |
|
||||
@@ -1338,6 +1338,7 @@ Settled by the user:
|
||||
| Bounty drafts (§21.2) | the official structure of each round from the user's 66 screenshots of 22 matches (R3 fixed sets, R9 boss groups, R11 seen 7-card lists; no multi-round card in a draft) | add a newly seen set / group / list to `tools/build-data.mjs BOUNTY_INITIAL_SETS / BOUNTY_BOSS_GROUPS / BOUNTY_HUNTER_GROUPS` and rebuild |
|
||||
| 机密商店 / 战术决策 repeats (§21.2) | the same card can be offered twice, as officially (the user, 2026-10-03: "机密商店按官方改成可以重复吧", "战术决策也按官方改成可以重复吧"); R11 机密商店 = two VI, ≥ 1 V, ≥ 1 盟约之币 | `choices.json shopDraft` / `tacticDraft` removed (choices.js falls back to the previous draws, still with replacement) |
|
||||
| 坚固维式重锤's scope (§21.21) | one 不死 lock per DEPLOYMENT: the carrier's own redeploy (after a knock-out, a 突袭 retreat + redeploy) re-arms it and ends a window still running (the user, 2026-10-03: "每次部署一次"; the text says only 首次); the lock belongs to the deployment, so a borrowed hammer (萨尔贡 × 娜仁图亚) follows the same rule | `content/items/battle.js deploymentOf` returning one key for the whole battle (one lock per battle) |
|
||||
| Six 重装 skills' trigger (§21.29) | 深巡 S2, 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 cast with an enemy in range (the basic strategy, DEFAULT) instead of the official 下半 TANK row's TAKE_DAMAGE — a deliberate, documented deviation (the owner, 2026-10-03: "反馈的人太多了"; GitHub issue #4, PR #12); every other MANUAL 重装 skill, 深巡 S1 included, waits for a hit | remove the entry from `tools/build-data.mjs TRIGGER_DEVIATIONS` (for 深巡 / 雷蛇 also PR #12's `trigger: 'DEFAULT'` kit line) and rebuild |
|
||||
| Multi-round bounties (§20.6, unchanged) | last two battles — now only 教鞭's card | `server/match/choices.js MULTI_ROUND_BOUNTY_BATTLES = null` |
|
||||
| §20.9's decisions | unchanged (start-deploy summons, 凯瑟琳's hand card, every blocker hits, the collider reach, the elite to the board, the fixed leader pool, the watched bond strip) | as §20.9 |
|
||||
|
||||
@@ -1393,7 +1394,7 @@ Not done (noted, no primary source or out of scope): whether 华法琳 S2's 流
|
||||
|
||||
- **Seen**: "干员深巡放在围栏围着的地块里不会主动攻击". **Official**: the fenced tiles are 围墙 (`tile_fence_bound`, "可放置近战单位,敌方无法进入") and 围栏 (`tile_fence`, "不可以通行") — LOW, buildable ALL, passable to flyers only; PRTS 围墙 / 围栏 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人"; 深巡 is a 哨戒铁卫 ("能够阻挡三个敌人,可以进行远程攻击", range 2-2); defender skills fire "受到伤害时" (PRTS 卫戍协议/帮助).
|
||||
- **Not reproduced**: from a fenced tile she attacks every targetable enemy on her range tiles — every deployable fenced tile of the 7 stages that have them, every direction facing walkable ground, normal and elite; the Final Assault halves and 联防; a real Match; the browser's battle (7 attacks, 1977 damage). Targeting reads range tiles only (no path or line-of-sight condition).
|
||||
- **Likely what players saw** (official): in some directions her 3-tile line faces only fence or forbidden tiles; 阻隔工事 crates hold some fenced tiles from the start (act1 m01: 3 of each 6-tile block; act1 m04, act2 m03); nothing hits her there, so her skill rarely fires; an own-tile melee unit never attacks from a fenced tile.
|
||||
- **Likely what players saw** (official): in some directions her 3-tile line faces only fence or forbidden tiles; 阻隔工事 crates hold some fenced tiles from the start (act1 m01: 3 of each 6-tile block; act1 m04, act2 m03); nothing hits her there, so her skill rarely fired (since §21.29 her default S2 casts with an enemy in range, a deliberate deviation; S1 still waits for a hit); an own-tile melee unit never attacks from a fenced tile.
|
||||
- **Fixed** (found while checking F4): a push or pull stops an enemy at the fence edge, 0.5 from the fenced unit — inside the 0.7071 ground radius — and 0.1.0 let that unit block it (薄绿 S2 on a fenced tile held the enemies she dragged). `_blockerFor` now refuses ground blocking from a tile ground units cannot pass (on the stages exactly the `b` tiles).
|
||||
- [ASSUMED]: §21.20. Tests: `test/sim/feedback1f-fence.test.js`; PLAYING §10 tip.
|
||||
|
||||
@@ -1441,3 +1442,15 @@ Two public GitHub issues after 0.1.0, both client-only. Normative lines rewritte
|
||||
- **The draft**: 查看禁用盟约与干员 under the order list opens `MatchInfoDialog` — `MatchInfo` in the components.js `Modal`, read-only, closed by 关闭, a tap outside or Esc (a press on the backdrop closes it on `mousedown`, so the click never reaches what lies under it — e.g. 确认选择). The draft runs on underneath: the dialog's footer repeats the turn and its seconds (`draftInfoStatus`: 轮到你决策 / X 决策中 + the step header's number, orange at ≤ 10 s; 已选择「…」after the own pick); a turn change (a pick, a skip, a turn that runs out, an AI pick) or the own pick closes it; the end of the draft unmounts it with the screen; it never changes the highlighted band (`g.bandFocus`) or the buttons.
|
||||
- **Sizes**: on desktop the briefing's own sizes (1920×1080: no scrolling); every size in the dialog is max(design rem, px floor), so on phones (1rem = 40 px; 756×366, 844×390) the text stays ≥ 9–15 px, the dialog fits the safe area and its body scrolls under a fixed header and footer.
|
||||
- Tests: `test/ui/match-info.test.js` (the model, greying by state, badges, tips, the legend, banned by tier, real 标准 / 绝境 matches, `draftInfoStatus`, briefing / draft / drawer reading the one module); opt-in `test/ui/match-info.e2e.test.js` (`SP_E2E=1`: solo 标准 on desktop — dialog = briefing, the three ways to close, a confirm afterwards, the in-game tab; co-op 绝境 with a desktop and a phone — the live countdown, a turn change closing it, 844×390 / 756×366 layout and scrolling, the draft's end unmounting it).
|
||||
|
||||
### 21.29 Deliberate deviation: six 重装 skills cast with an enemy in range (GitHub issue #4, PR #12) — `tools/build-data.mjs TRIGGER_DEVIATIONS / resolveTrigger / validateAll`, data/chess.json `skills[].trigger`, `kits/tier1.js` (深巡, 雷蛇), `kits/tier2.js` (灰毫), `kits/tier5.js` (号角)
|
||||
|
||||
- **Reported**: public issue #4 — 深巡 (哨戒铁卫, range 2-2) never cast her default S2 行动能力剥夺 until something hit her, in practice until she blocked; PR #12 (merged on GitHub) gave 深巡 S2 and 雷蛇 S2 `trigger: 'DEFAULT'` in their kits. Many players asked the same of other defenders; the owner (2026-10-03): "反馈的人太多了".
|
||||
- **Official, unchanged**: the 下半 (act2autochess, 2026-03) `skillTriggerDataList` row `TANK | | | 0 | TAKE_DAMAGE` and PRTS 卫戍协议/帮助's 重装 strategy "不受技能范围影响,受到伤害时释放技能" (下半, 2026/3/14) — a class row, covering every MANUAL 重装 skill whatever its index (§20.2); 上半 (act1autochess, 2025-11) had no TANK row, so 重装 skills used the basic strategy then (research 03 §1.4 keeps the history). PR #12's reasons — the row as "skill 1 only" and "offensive skills activate when an enemy is in their skill range" — rested on a community summary (arknights.wiki.gg), not on the CN client or PRTS; that quote is removed from research 03 (promised on PR #12).
|
||||
- **Decision** (the owner, 2026-10-03): a deliberate, documented deviation — six offensive 重装 skills take the basic strategy (DEFAULT: SP ready, about to attack, an enemy inside the initial range or one it blocks): **深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令 and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击**. Every other MANUAL 重装 skill keeps TAKE_DAMAGE — 深巡 S1 侵袭破坏应对 included.
|
||||
- **Where**: in the data, so the server, the browser battle and the bots read one rule. `TRIGGER_DEVIATIONS` is keyed by the normal chess id and the skill id (the elite record follows through its `baseId`), never by the skill id alone: 灰毫 S1 is the generic `skcom_atk_up[3]`, which 惊蛰, 幽灵鲨, 耶拉, 莫斯提马 and 莱恩哈特 carry too. `resolveTrigger` applies it last; the 12 records read `trigger { rule: 'DEFAULT', rawRule: 'TAKE_DAMAGE' }` — `rawRule` keeps the official row for traceability — and `validateAll` fails the build when an entry no longer meets a TAKE_DAMAGE row. PR #12's kit lines (`trigger: 'DEFAULT'` on 深巡 / 雷蛇 S2) stay, agreeing with the data (a test pins kit = data); their comments and the contributor's tests now give this section's reason.
|
||||
- **Measured** (a real battle: 战场#01(下半), one 萨卡兹枯朽前锋 walking the lane from the gate, the operator SP-full on (9,5) facing it): 深巡 / 雷蛇 S2 cast 6.2 s after the spawn, when the enemy steps onto the 2-2 line; 号角 S2 / S3 and 灰毫 S1 / S2 within 1.3 s (their 要塞 range reaches the gate) — the enemy reaches and first hits them at 23.8 s; each of the other 46 MANUAL 重装 records (normal + elite) casts on that first hit.
|
||||
- **Players notice**: these six fire sooner and more often — from range, before anything reaches the defender; 号角 S3's 过载 HP drain therefore starts sooner; 灰毫 S2 (block 0) lets an enemy pass as soon as one comes into range, not after the first hit; on a fenced tile (§21.23) 深巡's S2 fires.
|
||||
- One-line flip: remove the entry from `TRIGGER_DEVIATIONS` (for 深巡 / 雷蛇 also the kit line) and rebuild.
|
||||
- Normative lines rewritten with it: §5.6 (`TAKE_DAMAGE`), §19.2 (灰毫 S2), §20.2 (players notice, the table), §21.20 (a settled row), §21.23 (深巡 on a fenced tile).
|
||||
- Tests: `test/sim/feedback1-tank-triggers.test.js` (the data, kit = data, the real battle for the six and for every other MANUAL 重装 record), `test/data.test.js`, `kits_t1t2` / `kits_alt_t1` (PR #12's), `kits_alt_t2` (灰毫 S2), `kits_t5` / `kits_alt_t5` (号角), `test/docs-consistency.test.js`.
|
||||
+2
-2
@@ -71,7 +71,7 @@
|
||||
- 作战中:海嗣、狼群、流形、支援装置开战时就在摆放的位置部署;医疗探机、诅咒娃娃开战时也在摆放的位置免费部署一次(持续 10 / 15 秒,和官方一样不需要先开技能),之后所属干员每次发动技能时再次在原位出现;所属干员被击倒、再部署回场时,技能给的那一个如果还没出现,会立刻出现。巫恋退场时诅咒娃娃随之撤退,赫默退场时已出现的医疗探机不受影响。医疗探机、诅咒娃娃、支援装置、海嗣没有摆放就不会出现;狼群、流形(战术家的援军)没有摆放时会在战术点出现。
|
||||
- **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**阻挡了就一定能打到**:任何干员(包括站在近战位的远程干员和召唤物)总能攻击自己阻挡的敌人,不管它在不在攻击范围内、在干员哪一侧,并且优先攻击它(医疗干员除外:咒愈师以外的医疗挡住敌人时照常治疗队友,不会攻击它;「停止攻击」类技能期间也除外;技能原文另有限制的也按原文:普罗旺斯二技能开启时不以生命值高于 80% 的敌人为目标,寒檀二技能的冰凌随机落在攻击范围内的地块上)。
|
||||
- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击;**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但它不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。
|
||||
- **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**重装干员**的手动技能在受到伤害时释放;执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放。**自动触发**的技能按其本身规则,例如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。
|
||||
- **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**重装干员**的手动技能在受到伤害时释放(例外:深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放,这是本作有意与官方不同的地方;深巡一技能仍要受到伤害);执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放。**自动触发**的技能按其本身规则,例如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。
|
||||
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。野鬃二技能沿她的朝向推开:正前方 45° 以内按上面的距离,身后、侧面或贴身的敌人只被轻推一下(约 0.4 格以内);琳琅诗怀雅三技能的金币则从她身边向外推开,落在她前方 2–4 格的范围(官方「范围2-4」)内的地面敌人和她阻挡的敌人身上。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
|
||||
- **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。
|
||||
- **敌人**:远程敌人的射程按官方从干员的碰撞体积(半径 0.25 格)算起,比数据上的攻击范围半径远 0.25 格——例如萨卡兹枯朽战车(射程 2.2)能打到中心 2.45 格以内、站在低地的干员。它的第一下攻击就是「秽蚀轰击」(在目标处留下半径 1.7、持续 10 秒的污染区:低地每秒 50、高台 25 点真实伤害,同名不叠加),被阻挡时普通攻击伤害翻倍。“萨科塔之翼”等血量第一次低于一半时会在原地小范围乱飞 5 秒(官方叫「恐惧」);叙拉古、妮芙等干员造成的恐惧会让敌人朝远离干员的方向逃跑。隐匿的敌人只有被阻挡(或被揭示)时才能被选为攻击目标,溅射、技能的圆形范围伤害也打不到没被挡住的隐匿敌人(和官方一样):深池逐火战士每次被击倒后先重生 1 秒,然后变成继续往前走的隐匿「余烬」(需要 5 次伤害击倒,护卫的「火灰」10 次),一般要用干员挡住它才能选中(挡住时它不再半透明),10 秒内没打掉就满血站起来;敌人重生时身上的减速、恐惧等状态会被清空、技能冷却从头开始,被恐惧的逐火战士变成余烬后也能被挡住;转译基底·α 原始形态受到的伤害全部无效,受到第 4 次物理伤害、第 4 次法术伤害或被阻挡时花 2 秒变成寻仇者、特战术师或幽灵,之后才能被击倒。
|
||||
@@ -135,4 +135,4 @@
|
||||
- 进入隐秘核心看的是全队的**激活层数总和**:后期可以针对性地堆层数。
|
||||
- 层数到 999 就满了,多余的叠层没有用,可以把资源转去别的盟约。
|
||||
- 领袖战注意**限伤**:单次 30 万以上的伤害会被整次作废。盟约叠到 999 层后,再叠加额外的伤害倍率(例如 999 层谢拉格再点亮奥术)反而可能让大招打出 0 伤害;队友的奥术等减益也会放大你的伤害。
|
||||
- **橙色围栏围起的地块**(官方「围墙」)可以部署干员,敌人走不进去:站在里面的干员照常攻击自己攻击范围内的敌人(摆放时选好朝向,让攻击范围对着敌人走的路),但**挡不住地面敌人**(被推、拉到围栏边的敌人也不会被它阻挡),也很少挨打——重装干员(如深巡)的手动技能要受到伤害才释放,在里面可能一直不放;只打自己脚下一格的干员(如角峰)在里面打不到任何敌人。
|
||||
- **橙色围栏围起的地块**(官方「围墙」)可以部署干员,敌人走不进去:站在里面的干员照常攻击自己攻击范围内的敌人(摆放时选好朝向,让攻击范围对着敌人走的路),但**挡不住地面敌人**(被推、拉到围栏边的敌人也不会被它阻挡),也很少挨打——多数重装干员的手动技能要受到伤害才释放,在里面可能一直不放(深巡的二技能是例外,攻击范围内有敌人就会释放);只打自己脚下一格的干员(如角峰)在里面打不到任何敌人。
|
||||
+3
-1
@@ -754,7 +754,9 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
触发…直至古米完成一次普通攻击的治疗" — her heal mode waits for its heal); `DEFAULT` with `trigger.allies` (+ `hpAtMost`,
|
||||
`grid`) = the basic rule **and** such an ally on the grid: the cast replaces the attack about to be made (塞雷娅 S1 "触发
|
||||
时会替换当次攻击", ≤ half HP); a cast whose ally condition fails before that attack is withdrawn, its charge returned;
|
||||
`TAKE_DAMAGE` — ready and just hit (重装: "不受技能范围影响,受到伤害时释放技能"); `SP_FULL` (`ALWAYS`) — immediately;
|
||||
`TAKE_DAMAGE` — ready and just hit (重装: "不受技能范围影响,受到伤害时释放技能"; in the data every MANUAL 重装 skill but
|
||||
the six of DESIGN §21.29 — 深巡 / 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2 — which are `DEFAULT`, a deliberate deviation);
|
||||
`SP_FULL` (`ALWAYS`) — immediately;
|
||||
`CUSTOM_RANGE` — an enemy inside `trigger.customRangeGrid` (rotated by the unit direction like every grid); `SEARCH` — an
|
||||
enemy inside the INITIAL range, checked every tick (no attack needed: "不受基础策略影响,在初始攻击范围内存在敌人时释放技能");
|
||||
`GDGLOW_SKILL_2` — "全场存在可选目标时释放技能": a targetable enemy anywhere (a heal skill: an ally that needs healing),
|
||||
|
||||
@@ -84,7 +84,7 @@ The visible chess per tier are 16/17/19/22/19/19. The item slot uses the same ti
|
||||
| Equipment | 2 per operator (board **or** hand). Locked; 3rd = replace, and the old one is destroyed. 2 identical normal items (equipped or not) merge into golden. | VERIFIED |
|
||||
| Deployment | Free, at t=0, order **top→bottom then left→right** (right-side boss player mirrored). Ranged units may use melee tiles. | VERIFIED |
|
||||
| DP | Start **10**, **+1/s**, cap **99**. Used only for auto-redeploy (original tile, respawn timer done, DP ≥ `cost`). | DATA/VERIFIED |
|
||||
| Skill auto-cast | Default: SP ready + about to attack/heal + an enemy in the **initial** range. ~~`skillTriggerDataList` rows with `skillIndex` 0 = skill 1 only (TANK S1 → on damage: only 古米 and 灰毫 this season)~~ — Superseded by user playtest #6 (DESIGN §20): the class rows apply to **every MANUAL skill** of the class and never to an AUTO skill (PRTS 卫戍协议/帮助 names whole classes; the 阵法术师 row must cover 薄绿's default S2, a phalanx that never attacks with its skill off); a MANUAL skill with its own 技能范围 (not an attack-range change) uses SKILL_RANGE; automatic operations have a 3 s cooldown. charId rows per 03. | VERIFIED (class-row reading superseded) |
|
||||
| Skill auto-cast | Default: SP ready + about to attack/heal + an enemy in the **initial** range. ~~`skillTriggerDataList` rows with `skillIndex` 0 = skill 1 only (TANK S1 → on damage: only 古米 and 灰毫 this season)~~ — Superseded by user playtest #6 (DESIGN §20): the class rows apply to **every MANUAL skill** of the class and never to an AUTO skill (PRTS 卫戍协议/帮助 names whole classes; the 阵法术师 row must cover 薄绿's default S2, a phalanx that never attacks with its skill off); a MANUAL skill with its own 技能范围 (not an attack-range change) uses SKILL_RANGE; automatic operations have a 3 s cooldown. charId rows per 03. The TANK row came with 下半 (上半 had none); 深巡 S2, 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 deliberately cast with an enemy in range instead (the owner's decision, DESIGN §21.29). | VERIFIED (class-row reading superseded; six 重装 skills deviate on purpose) |
|
||||
| Speed | 2× forced, no manual control | VERIFIED |
|
||||
| Board geometry | 19×21 stage. Player board rows 9–12, cols 2–10. Gates (9,10) and (12,10); R1–3 use only the lower one. Goal (9,2). Col 9 is a non-deployable lane. Hand row 7 cols 0–9. Temp hand row 8 cols 4–8. Boss field rows 1–5. | DATA |
|
||||
| Bond counting | Distinct operators on board. 远见/奇迹/投资人 also count the hand. 绝技 counts elites. Layers persist all match; **no IN_BATTLE layer gains in 联防 or boss rounds**; each bond's layers stop at **999** (research 11). | DATA/VERIFIED |
|
||||
|
||||
@@ -71,18 +71,17 @@ All skills fire automatically in this mode. Normal behaviour: AUTO skills fire b
|
||||
| NONE | | char_1051_headb2 | 2 | CUSTOM_RANGE_SEARCH_ENEMY |
|
||||
| NONE | | char_2025_shu | 2 | TRY_SEARCH_ALLY_SKILL |
|
||||
|
||||
Meaning (names are from the data; semantics partly inferred):
|
||||
Community rule text (arknights.wiki.gg/wiki/Stronghold_Protocol, matches the in-game help): "Offensive skills activate when an enemy is in their Skill Range. Defensive skills activate when that unit is receiving damage. Healing skills activate when a friendly unit within range is damaged while skill is charged." Mapping to data:
|
||||
Meaning (names are from the data; semantics partly inferred — the official wording, PRTS 卫戍协议/帮助 §作战阶段 技能操作, is quoted in DESIGN §20.2):
|
||||
|
||||
- `DEFAULT` (no matching row; 97 of 129 pool chess): offensive -> SP full AND >=1 enemy inside the skill range (skill `rangeId` if the skill overrides range, else the operator's attack range). Healing skills (MEDIC, heal-type skills) -> SP full AND an injured ally inside range. Buff/other skills [ASSUMED] -> same as offensive (enemy in range).
|
||||
- `TAKE_DAMAGE` (profession TANK, 15 chess): SP full AND the unit is currently receiving damage. The row has skillIndex 0 but pool tanks use S1 x2, S2 x10, S3 x4; the wiki rule ("defensive skills") implies it is a **wildcard for any skill index** in profession/subprofession rows — use that interpretation.
|
||||
- `DEFAULT` (no matching row; 97 of 129 pool chess): the basic strategy "技能就绪,且即将进行普通攻击/治疗" — SP full AND about to attack (heal skills: to heal) AND an enemy (an injured ally) inside the initial range; a MANUAL skill with a 技能范围 of its own (not an attack-range change) uses `SKILL_RANGE` instead (DESIGN §20.2).
|
||||
- `TAKE_DAMAGE` (profession TANK, 15 chess): SP full AND the unit takes damage — PRTS 重装 "不受技能范围影响,受到伤害时释放技能". The row has skillIndex 0 but pool tanks use S1 x2, S2 x10, S3 x4; as a class row it covers every MANUAL skill of the class (DESIGN §20.2; C1 below keeps the "skill 1 only" counter-reading). **History of the row** (activity_table `autoChessData.skillTriggerDataList`): 上半 (act1autochess, 2025-11) shipped no TANK row — 重装 skills had no strategy of their own; 下半 (act2autochess, 2026-03-14) added `TANK | | | 0 | TAKE_DAMAGE` for every skill index, with PRTS's 重装 line (下半, 2026/3/14). **Deliberate deviation** (the owner, 2026-10-03, after community feedback — GitHub issue #4, PR #12; DESIGN §21.29): 深巡 S2, 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 cast with an enemy in range (`DEFAULT`; `tools/build-data.mjs TRIGGER_DEVIATIONS`, keyed per chess — 灰毫 S1 is the generic `skcom_atk_up[3]` — with `rawRule` keeping the official TAKE_DAMAGE); every other MANUAL 重装 skill, 深巡 S1 included, keeps TAKE_DAMAGE.
|
||||
- `ALWAYS` (5 chess; subprof bearer 旗手 / tactician 战术家 / bard 吟游者, plus 维什戴尔 S3 not in pool): cast as soon as SP is full, no target needed. Pool: 伺夜 S3, 缪尔赛思 S3 (tactician), 魔王 S3 (T4 & hidden T5), 浊心斯卡蒂 S3 (bard).
|
||||
- `SEARCH` (4 chess; subprof librator 解放者 / phalanx 阵法术师, 安洁莉娜 S2/S3): [ASSUMED] cast when SP full and any enemy is on the field (search whole map) — phalanx/librator skills change their own attack mode and should not wait for an enemy in their (small/none) normal range. Pool: 薄绿 S2, 蜜蜡 S2 (hidden), 卡涅利安 S2 (phalanx); 安洁莉娜 S3. 玛恩纳 S3 and 圣聆初雪 S3 are overridden by their charId rows (CUSTOM_RANGE).
|
||||
- `CUSTOM_RANGE_SEARCH_ENEMY` (6 chess): SP full AND an enemy inside the `skillRangeDict` range (listed below). Pool: 乌尔比安 S3 (6-1), 维娜·维多利亚 S3 (x-4), 余 S3 (3-15), 远牙 S3 (6-1), 玛恩纳 S3 (3-18), 圣聆初雪 S3 (x-2). Not in pool: 塑心 demetr, 司霆惊蛰 leizi2, 林 lin, 猎蜂? headb2.
|
||||
- `GDGLOW_SKILL_2` (2 chess: 荒芜拉普兰德 whitw2 S3, 纯烬艾雅法拉 agoat2 S3; 澄闪 gdglow not in pool): named after 澄闪 S3 whose skill places/targets a zone; [ASSUMED] cast when SP full and an enemy is inside the attack range, choosing the target tile/enemy with the most enemies around it.
|
||||
- `MLYSS_WTRMAN` (缪尔赛思, skillIndex -1): AI rule for her summon 流形 (token_10030_mlyss_wtrman) placement/behaviour [ASSUMED: token copies/follows the nearest allied operator as in the base game]; her S3 itself uses the tactician ALWAYS rule.
|
||||
- `MARCILS2` (玛露西尔 S2) and `TRY_SEARCH_ALLY_SKILL` (黍 S3) are for operators not in this season's pool.
|
||||
- Redeployment (same wiki page): a knocked-out operator auto-redeploys on its original tile when its `respawnTime` has elapsed AND there is enough DP for its `cost` (or instantly via effects). So **cost and respawnTime matter** in battle; see the battle-rules research for the DP economy.
|
||||
- Redeployment (PRTS 卫戍协议/帮助 §作战阶段 单位部署, quoted in research 01 §4.3): a knocked-out operator auto-redeploys on the tile it lies on when its `respawnTime` has elapsed AND there is enough DP for its `cost` (or instantly via effects). So **cost and respawnTime matter** in battle; see the battle-rules research for the DP economy.
|
||||
|
||||
skillRangeDict (custom trigger ranges; `O` = operator tile, `#` = trigger tile, facing right):
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
// Operator loadouts (DESIGN §16): every selectable non-default skill of the 16 visible chess is authored in the kit's
|
||||
// `skills: { [skillId]: SkillSpec }` map from its own SkillRecord (skillRec / skillBbOf — Lv4 normal, Lv7 elite);
|
||||
// talents / traits read the resolved record, so a module choice ('none' ⇒ traitBase / talentsBase, module.active
|
||||
// false) is honoured. Per-skill triggers come from data (the official 技能策略: every MANUAL 重装 skill ⇒ TAKE_DAMAGE, a
|
||||
// MANUAL skill with its own 技能范围 ⇒ SKILL_RANGE, AUTO skills keep their own rule); the few spec overrides are documented
|
||||
// at the skill (冲锋号令 AUTO ⇒ SP_FULL, 花香疗法 heal-type DEFAULT; the two 哨戒铁卫 S2s 深巡 行动能力剥夺 and 雷蛇 反击电弧
|
||||
// override the 重装 TAKE_DAMAGE row ⇒ DEFAULT — an offensive skill fires with an enemy in range, research 03 §1.4).
|
||||
// false) is honoured. Per-skill triggers come from data (the official 技能策略: every MANUAL 重装 skill ⇒ TAKE_DAMAGE —
|
||||
// but the six of the owner's deliberate deviation, DESIGN §21.29 / tools/build-data.mjs TRIGGER_DEVIATIONS, which are
|
||||
// DEFAULT —, a MANUAL skill with its own 技能范围 ⇒ SKILL_RANGE, AUTO skills keep their own rule); the few spec rules are
|
||||
// documented at the skill (冲锋号令 AUTO ⇒ SP_FULL, 花香疗法 heal-type DEFAULT; the two 哨戒铁卫 S2s 深巡 行动能力剥夺 and
|
||||
// 雷蛇 反击电弧 state the DEFAULT their data carries since that deviation — GitHub issue #4, PR #12).
|
||||
// Tests: test/content/kits_alt_t1.test.js.
|
||||
//
|
||||
// fx kinds emitted (battle.fx(kind, {x, y, …})): aoe {radius, id, skill} · zone {radius, dur, id, skill} ·
|
||||
@@ -397,10 +398,11 @@ export default {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 1_04 深巡 行动能力剥夺: longer line range (skill grid), ATK +atk, ASPD +attack_speed, fin darts pierce
|
||||
// attack@max_target enemies on the line and cause attack@sluggish s of 停顿.
|
||||
// 技能策略 override → DEFAULT: the official class row hands a 重装 S2 TAKE_DAMAGE, which makes a 2-2 ranged 哨戒铁卫
|
||||
// wait until something hits her — in practice until she blocks (issue #4). 行动能力剥夺 is an offensive ranged skill,
|
||||
// so the basic strategy (SP ready + about to attack + an enemy inside the initial range) is the right rule. Only the
|
||||
// rule changes: spCost / initSp / spType still come from data.
|
||||
// 技能策略 → DEFAULT (PR #12; DESIGN §21.29): the official 下半 class row hands every MANUAL 重装 skill TAKE_DAMAGE,
|
||||
// which makes a 2-2 ranged 哨戒铁卫 wait until something hits her — in practice until she blocks (GitHub issue #4). By
|
||||
// the owner's deliberate deviation from that row (2026-10-03, community feedback) this offensive ranged skill takes
|
||||
// the basic strategy (SP ready + about to attack + an enemy inside the initial range); her data says DEFAULT too
|
||||
// (rawRule keeps the official TAKE_DAMAGE). Only the rule changes: spCost / initSp / spType still come from data.
|
||||
// 细胞活性抑制剂: attacks inflict `damage` arts per `interval` s for `duration` s (damage_seamonster vs 【海怪】).
|
||||
// Elite module (SPT-X): stealth of enemies inside the range is cancelled.
|
||||
// Alternate S1 侵袭破坏应对 (重装 ⇒ TAKE_DAMAGE trigger from data): ATK +atk, DEF +def.
|
||||
@@ -999,10 +1001,10 @@ export default {
|
||||
// 1_20 雷蛇 反击电弧 (hurt SP): attack interval ×(1 + base_attack_time) (PRTS "攻击间隔增大(+70%)": a RATIO for this skill,
|
||||
// 1.2 → 2.04 s — not +0.7 s), ATK +atk, arts attacks on up to attack@max_target enemies,
|
||||
// attack@buff_prob to stun attack@stun s; afterwards 雷蛇 is stunned `stun` s.
|
||||
// 技能策略 override → DEFAULT: 反击电弧 is an offensive skill (ATK +125 %, arts hits on up to 3 enemies, stun), and the
|
||||
// mode's rule is "offensive skills activate when an enemy is in their skill range" (research 03 §1.4, the in-game
|
||||
// help). The TANK row is a TAKE_DAMAGE row documented for skillIndex 0; reading it as a wildcard made this S2 — and
|
||||
// 深巡's — wait for a hit instead (issue #4). Both 哨戒铁卫 S2s therefore override it here.
|
||||
// 技能策略 → DEFAULT (PR #12; DESIGN §21.29): 反击电弧 is an offensive skill (ATK +125 %, arts hits on up to 3 enemies,
|
||||
// stun); the official 下半 TANK row (TAKE_DAMAGE for every MANUAL 重装 skill) made it — and 深巡's S2 — wait for a hit
|
||||
// (GitHub issue #4). The owner's deliberate deviation from that row (2026-10-03, community feedback) gives both
|
||||
// 哨戒铁卫 S2s the basic strategy; the data says DEFAULT too (rawRule keeps the official TAKE_DAMAGE).
|
||||
// 战术防御: when attacked, +sp SP to herself and to one random ally in the talent grid. Elite 雷抗: RES +magic_resistance.
|
||||
// Elite module (SPT-X): stealth of enemies inside the range is cancelled.
|
||||
// Alternate S1 充能防御 (AUTO, hurt SP, "技能自动开启" — an AUTO skill takes no 技能策略: SP_FULL, so the hit that fills SP
|
||||
|
||||
@@ -789,7 +789,8 @@ export default {
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 2_18 灰毫 攻击力强化·γ型 (TAKE_DAMAGE): ATK +atk. 炮术研习: ATK +atk, or +ashlok_t_1.atk when the `cnt` orthogonal
|
||||
// 2_18 灰毫 攻击力强化·γ型 (DEFAULT, like S2: the owner's deliberate deviation from the 重装 TAKE_DAMAGE row, data
|
||||
// TRIGGER_DEVIATIONS, DESIGN §21.29): ATK +atk. 炮术研习: ATK +atk, or +ashlok_t_1.atk when the `cnt` orthogonal
|
||||
// tiles around her are all ground (LOW). Elite module (FOR-X, trait atk_scale): vs blocked enemies ATK ×atk_scale.
|
||||
// S2 专注轰击 (alt): block count 0 (noBlock: releases what she holds), only ranged (splash) attacks, base attack time
|
||||
// +base_attack_time s (−0.4 / −0.5 on 2.8), ATK +atk.
|
||||
|
||||
@@ -1150,6 +1150,8 @@ const KITS = {
|
||||
// 隐匿). S2 暴风号令 (ammo 10): every attack attack@s2.atk_scale × ATK phys splash; 过载 for the second half of the
|
||||
// ammo [ASSUMED like S3]: + attack@s2.magic_atk_scale × ATK arts to every enemy hit (manual close never happens in the
|
||||
// auto battle). Module FOR-Y (elite): ASPD +10 while not blocking.
|
||||
// S2 and S3 cast with an enemy in range (data DEFAULT, rawRule TAKE_DAMAGE): the owner's deliberate deviation from the
|
||||
// official 重装 TAKE_DAMAGE row (DESIGN §21.29, tools/build-data.mjs TRIGGER_DEVIATIONS).
|
||||
chess_char_5_08_a: (bb, chess, def) => {
|
||||
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess), tm = talent(chess, -1);
|
||||
const sid = selectedId(chess, def);
|
||||
|
||||
@@ -673,7 +673,8 @@ test('1_20 雷蛇 反击电弧: attack interval +70 % of the base (PRTS 增大(+
|
||||
for (const id of pair('20')) {
|
||||
const bb = raw(id).skill.bb;
|
||||
assert.equal(raw(id).skill.skillId, 'skchr_liskam_2');
|
||||
// 重装 S2 反击电弧 is an OFFENSIVE skill: the basic strategy casts it with an enemy in the skill range (issue #4) —
|
||||
// 重装 S2 反击电弧 is an OFFENSIVE skill: the basic strategy casts it with an enemy in the skill range (issue #4; the
|
||||
// deliberate deviation from the official 重装 row, DESIGN §21.29) —
|
||||
// the enemy here stands two tiles ahead and, being a speed-0 dummy, never touches her
|
||||
const h = run({ defs: { enemies: { e: dummy('e', { atk: 300, bat: 1 }) }, chess: noGarrison(id) }, units: [{ chessId: id, row: 9, col: 4, carryState: READY }], enemies: [{ key: 'e', pos: [9, 6] }] });
|
||||
const u = h.unit(id);
|
||||
|
||||
@@ -456,7 +456,7 @@ test('2_17 折桠 S1 绝境抵抗: TAKE_DAMAGE; DEF +def and 抵抗 (control sta
|
||||
}
|
||||
});
|
||||
|
||||
test('2_18 灰毫 S2 专注轰击: cast when hit (重装 TAKE_DAMAGE), then blocks nothing, ranged splash bombs only, shorter interval, ATK +atk', () => {
|
||||
test('2_18 灰毫 S2 专注轰击: cast with an enemy in range (DEFAULT, the deliberate deviation from the 重装 row — DESIGN §21.29), then blocks nothing, ranged splash bombs only, shorter interval, ATK +atk', () => {
|
||||
for (const id of both('chess_char_2_18')) {
|
||||
const bb = bbAlt(id);
|
||||
const enemies = { w: enemyRec({ key: 'w', hp: 1e7, speed: 2, atk: 50, bat: 1 }) };
|
||||
@@ -465,15 +465,18 @@ test('2_18 灰毫 S2 专注轰击: cast when hit (重装 TAKE_DAMAGE), then bloc
|
||||
assert.ok(c.runUntil(() => c.enemies()[0]?.blockedBy === c.unit(id), 10), 'default: blocked');
|
||||
done(c);
|
||||
|
||||
// (a second walker comes by while the skill runs: she bombs it from range, unblocked)
|
||||
const h = run({ defs: { enemies }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'w', route: 0 }, { key: 'w', route: 0, time: 5 }] });
|
||||
// (a second walker, 2 s behind the first, comes by while the skill runs — 10 s from the cast, made as the first one
|
||||
// comes into range: she bombs it from range, unblocked)
|
||||
const h = run({ defs: { enemies }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'w', route: 0 }, { key: 'w', route: 0, time: 2 }] });
|
||||
const u = h.unit(id);
|
||||
usesAlt(u, id);
|
||||
assert.equal(u.skill.rule, 'TAKE_DAMAGE', 'a MANUAL 重装 skill: "不受技能范围影响,受到伤害时释放技能"');
|
||||
// the official 下半 重装 row (TAKE_DAMAGE) is overridden for this skill by the owner's decision (data rawRule keeps it)
|
||||
assert.equal(u.skill.rule, 'DEFAULT', 'an offensive 重装 skill of the deviation: the basic strategy');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 8));
|
||||
const st = h.b.time;
|
||||
const atk0 = h.eventsOf('atk').length;
|
||||
assert.ok(h.hooksOf('damaged').some((x) => x.target === u), 'cast by the hit of the enemy she blocked');
|
||||
assert.equal(started(h, u)[0].reason, 'DEFAULT', 'cast by the basic strategy');
|
||||
assert.ok(!h.hooksOf('damaged').some((x) => x.target === u), 'with the walker in range, before anything hit her');
|
||||
assert.ok(u.s.flags.noBlock);
|
||||
approx(u.s.atk, u.base.atk * (1 + u.findBuff('talent:ashlok').mods.atkPct + bb.atk), 'ATK (+ 炮术研习)');
|
||||
approx(u.s.interval, (u.base.bat + bb.base_attack_time) * 100 / u.s.aspd, 'base attack time shortened');
|
||||
|
||||
@@ -527,18 +527,19 @@ test('号角 S1 照明榴弹 (自动触发 ⇒ DEFAULT): the next ranged shot de
|
||||
}
|
||||
});
|
||||
|
||||
test('号角 S2 暴风号令: 10 rounds of attack@s2.atk_scale × ATK splash; the second half adds magic_atk_scale × ATK arts (过载)', () => {
|
||||
test('号角 S2 暴风号令: cast with an enemy in range (DEFAULT, DESIGN §21.29); 10 rounds of attack@s2.atk_scale × ATK splash; the second half adds magic_atk_scale × ATK arts (过载)', () => {
|
||||
for (const id of pair('08')) {
|
||||
const h = run({ defs: { enemies: { enemy_dummy: dummy('enemy_dummy') } }, units: [entry(id, 'skchr_horn_2', { row: 10, col: 2 })], enemies: [{ key: 'enemy_dummy', pos: [10, 6] }] });
|
||||
const u = sel(h, id, 'skchr_horn_2');
|
||||
const bb = bbOf(u);
|
||||
const e = h.b.enemies[0];
|
||||
h.step();
|
||||
// the owner's deliberate deviation from the 重装 TAKE_DAMAGE row (DESIGN §21.29): cast with an enemy in range
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
u.skill.gainSp(1000);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, '重装 TAKE_DAMAGE: not before a hit');
|
||||
h.b.dealDamage(e, u, { amount: 10, type: 'phys' });
|
||||
assert.equal(u.skill.activations, 1, 'cast by the hit');
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), 'cast with the enemy in range');
|
||||
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT');
|
||||
assert.ok(!h.hooksOf('damaged').some((c) => c.target === u), 'no hit needed');
|
||||
assert.equal(u.skill.kind, 'ammo');
|
||||
assert.equal(u.skill.ammoLeft + h.hooksOf('ammoUsed').filter((c) => c.unit === u).length, bb['attack@s2.trigger_time']);
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 60));
|
||||
|
||||
@@ -132,7 +132,8 @@ test('1_04 深巡: fin darts pierce attack@max_target enemies on the line + 1 s
|
||||
});
|
||||
const u = h.unit(id);
|
||||
h.step();
|
||||
// 技能策略 (issue #4): an enemy inside her initial 2-2 range is enough — the basic strategy casts it, no hit required
|
||||
// 技能策略 (issue #4; the deliberate deviation of DESIGN §21.29): an enemy inside her initial 2-2 range is enough — the
|
||||
// basic strategy casts it, no hit required
|
||||
assert.equal(u.skill.rule, 'DEFAULT', '行动能力剥夺 is an offensive ranged skill, not a 重装 TAKE_DAMAGE one');
|
||||
h.run(3.5);
|
||||
assert.ok(u.skill.active);
|
||||
@@ -150,9 +151,9 @@ test('1_04 深巡: fin darts pierce attack@max_target enemies on the line + 1 s
|
||||
});
|
||||
|
||||
test('1_04 深巡 / 1_20 雷蛇 S2 技能策略: both 哨戒铁卫 S2s cast with an enemy in range, no hit needed (issue #4)', () => {
|
||||
// both S2s are offensive (深巡: range up + ATK/ASPD + piercing darts; 雷蛇: ATK +125 % + arts on 3 + stun), and the
|
||||
// mode's rule is "offensive skills activate when an enemy is in their skill range". The 重装 TAKE_DAMAGE row is
|
||||
// documented for skillIndex 0 only — reading it as a wildcard made both wait for a hit.
|
||||
// both S2s are offensive (深巡: range up + ATK/ASPD + piercing darts; 雷蛇: ATK +125 % + arts on 3 + stun); the official
|
||||
// 下半 重装 row (TAKE_DAMAGE for every MANUAL 重装 skill) made both wait for a hit, and the owner's deliberate deviation
|
||||
// from it (DESIGN §21.29, after community feedback) gives them the basic strategy — the data and the kit say DEFAULT.
|
||||
for (const id of ['chess_char_1_04_a', 'chess_char_1_04_b', 'chess_char_1_20_a', 'chess_char_1_20_b']) {
|
||||
const h = run({ units: [{ chessId: id, row: 9, col: 4, carryState: READY }], enemies: [{ key: 'e', pos: [9, 6] }] });
|
||||
const u = h.unit(id);
|
||||
@@ -1116,7 +1117,7 @@ test('2_17 折桠: 生存决心 trembles ground enemies around, ATK/DEF up, hits
|
||||
done(h2);
|
||||
});
|
||||
|
||||
test('2_18 灰毫: 炮术研习 ATK +atk (ground surroundings: ashlok_t_1.atk); TAKE_DAMAGE skill ATK +atk; elite ×1.1 vs blocked', () => {
|
||||
test('2_18 灰毫: 炮术研习 ATK +atk (ground surroundings: ashlok_t_1.atk); skill ATK +atk cast with an enemy in range (DEFAULT, DESIGN §21.29); elite ×1.1 vs blocked', () => {
|
||||
const id = 'chess_char_2_18_a', bb = bbOf(id), t = tal(id);
|
||||
const h = run({ units: [{ chessId: id, row: 10, col: 5 }, { chessId: 'chess_char_2_18_b', row: 10, col: 3 }] });
|
||||
h.step();
|
||||
@@ -1126,7 +1127,11 @@ test('2_18 灰毫: 炮术研习 ATK +atk (ground surroundings: ashlok_t_1.atk);
|
||||
const h2 = run({ defs: { enemies: { e: dummy('e', { atk: 400 }) } }, units: [{ chessId: id, row: 10, col: 5, carryState: READY }], enemies: [{ key: 'e', pos: [10, 5] }] });
|
||||
const u = h2.unit(id);
|
||||
h2.runUntil(() => u.skill.active, 5);
|
||||
assert.equal(h2.hooksOf('skillStart').find((c) => c.unit === u).reason, 'TAKE_DAMAGE');
|
||||
// the owner's deliberate deviation from the 重装 TAKE_DAMAGE row: the basic strategy, no hit needed — she casts before the
|
||||
// enemy she holds has hit her
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
assert.equal(h2.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT');
|
||||
assert.ok(!h2.hooksOf('damaged').some((c) => c.target === u), 'not hit before the cast');
|
||||
approx(u.s.atk, u.base.atk * (1 + t['ashlok_t_1.atk'] + bb.atk));
|
||||
done(h2);
|
||||
const idb = 'chess_char_2_18_b', tb = tbOf(idb);
|
||||
|
||||
@@ -364,7 +364,7 @@ test('史尔特尔 S3 黄昏: full heal, max HP +5000, ATK +210 %, range +2, 3 t
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
test('号角 S3: ATK +25 %, BAT −1.2 s, then overload ATK +50 % with HP loss; T1 Defenders ATK +20 %; T2 血战', () => {
|
||||
test('号角 S3: cast with an enemy in range (DEFAULT, DESIGN §21.29); ATK +25 %, BAT −1.2 s, then overload ATK +50 % with HP loss; T1 Defenders ATK +20 %; T2 血战', () => {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { enemy_dummy: dummy() }, chess: { t_tank: ally('t_tank', { profession: 'TANK', stats: { atk: 100 } }) } },
|
||||
units: [{ chessId: 'chess_char_5_08_a', row: 9, col: 3 }, { chessId: 't_tank', row: 12, col: 3 }],
|
||||
@@ -376,10 +376,12 @@ test('号角 S3: ATK +25 %, BAT −1.2 s, then overload ATK +50 % with HP loss;
|
||||
approx(h.unit('t_tank').s.atk, 100 * (1 + t0.atk), 1e-6, '军事要塞 on another Defender');
|
||||
approx(u.s.atk, u.base.atk * (1 + t0.atk), 1e-6, '… and on herself');
|
||||
u.skill.gainSp(1000);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, '重装 TAKE_DAMAGE: not before a hit');
|
||||
h.b.dealDamage(h.enemies()[0], u, { amount: 10, type: 'phys' });
|
||||
// cast at her next attack with the enemy in range, no hit needed: DEFAULT — the owner's deliberate deviation from the
|
||||
// 重装 TAKE_DAMAGE row (DESIGN §21.29)
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 5));
|
||||
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT');
|
||||
assert.equal(u.stats.taken, 0, 'nothing hit her');
|
||||
const start = h.b.time;
|
||||
const total = u.def.skill.duration, ov = bb['horn_s_3[overload_start].damage_duration'];
|
||||
approx(u.s.atk, u.base.atk * (1 + t0.atk + bb.atk));
|
||||
|
||||
@@ -469,6 +469,28 @@ test('chess: skills[] = every skill unlocked at the status, at the chess skill l
|
||||
assert.deepEqual(chess.chess_char_1_08_a.skills[1].trigger.customRangeGrid, chess.chess_char_1_08_a.skills[1].rangeGrid);
|
||||
assert.deepEqual(rules('chess_char_4_22_a'), ['DEFAULT', 'DEFAULT', 'DEFAULT']); // 银灰: "攻击范围缩小 / 扩大" = attack range
|
||||
assert.deepEqual(rules('chess_char_3_18_a'), ['DEFAULT', 'SKILL_RANGE', 'DEFAULT']); // 忍冬: S2 对周围…, S3 攻击距离+1
|
||||
// the deliberate deviation from the 重装 row (DESIGN §21.29, the owner's decision; tools/build-data.mjs
|
||||
// TRIGGER_DEVIATIONS, per chess): six skills cast with an enemy in range, rawRule keeps the official TAKE_DAMAGE
|
||||
const raws = (id) => chess[id].skills.map((s) => s.trigger.rawRule);
|
||||
for (const id of ['chess_char_1_04_a', 'chess_char_1_04_b']) { // 深巡: S1 keeps the row, S2 deviates
|
||||
assert.deepEqual(rules(id), ['TAKE_DAMAGE', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['TAKE_DAMAGE', 'TAKE_DAMAGE']);
|
||||
}
|
||||
for (const id of ['chess_char_1_20_a', 'chess_char_1_20_b']) { // 雷蛇: S1 AUTO, S2 deviates
|
||||
assert.deepEqual(rules(id), ['DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['DEFAULT', 'TAKE_DAMAGE']);
|
||||
}
|
||||
for (const id of ['chess_char_5_08_a', 'chess_char_5_08_b']) { // 号角: S1 AUTO, S2 / S3 deviate
|
||||
assert.deepEqual(rules(id), ['DEFAULT', 'DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['DEFAULT', 'TAKE_DAMAGE', 'TAKE_DAMAGE']);
|
||||
}
|
||||
for (const id of ['chess_char_2_18_a', 'chess_char_2_18_b']) { // 灰毫: S1 (generic skcom_atk_up[3]) and S2
|
||||
assert.deepEqual(rules(id), ['DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['TAKE_DAMAGE', 'TAKE_DAMAGE']);
|
||||
}
|
||||
const deviated = Object.values(chess).flatMap((c) => (c.skills || []).filter((s) => s.trigger.rawRule === 'TAKE_DAMAGE' && s.trigger.rule !== 'TAKE_DAMAGE').map((s) => `${c.baseId} ${s.skillId}`));
|
||||
assert.equal(deviated.length, 12, 'exactly the six skills, normal + elite');
|
||||
assert.deepEqual([...new Set(deviated)].sort(), ['chess_char_1_04_a skchr_udflow_2', 'chess_char_1_20_a skchr_liskam_2', 'chess_char_2_18_a skchr_ashlok_2', 'chess_char_2_18_a skcom_atk_up[3]', 'chess_char_5_08_a skchr_horn_2', 'chess_char_5_08_a skchr_horn_3']);
|
||||
for (const c of Object.values(chess)) {
|
||||
for (const s of c.skills || []) {
|
||||
if (s.skillType !== 'MANUAL') assert.ok(!['TAKE_DAMAGE', 'SEARCH', 'SKILL_RANGE'].includes(s.trigger.rule), `${c.chessId} ${s.skillId}: an AUTO / PASSIVE skill takes no strategy row`);
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
// 999 layer cap (one implementation) and 限伤 300000, the 假想敌:胄 kit — and the normative lines they rewrote; player
|
||||
// feedback after 0.1.0 (DESIGN §21, v0.1.1) including batch 6 (§21.21–§21.25: 坚固维式重锤 once per deployment, 起飞,
|
||||
// fenced tiles, knocked-out bodies, the dispatcher snapshot and the manifest shrink guard credited to PR #2 / PR #7) and
|
||||
// the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2).
|
||||
// the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2),
|
||||
// the closing additions §21.26–§21.28 (GitHub issues #1 / #5 / #8) and the owner's deliberate trigger deviation for six
|
||||
// 重装 skills (§21.29, GitHub issue #4 / PR #12).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -791,3 +793,35 @@ test('突变细胞 after the WA merge (DESIGN §21.1): the carrier is destroyed,
|
||||
assert.match(PS, /const np = this\.acquireChess\(newId, \{ source: 'transform' \}\);/);
|
||||
assert.ok(!/fromKey/.test(doc('server/match/audit.js')), 'audit.js: no exception for a transformed carrier\'s tile');
|
||||
});
|
||||
|
||||
test('the deliberate trigger deviation (DESIGN §21.29): six 重装 skills DEFAULT in the builder, the data and every doc; research 03 keeps the history, not the wiki.gg summary', () => {
|
||||
const sub = DESIGN.slice(DESIGN.indexOf('### 21.29 '));
|
||||
for (const re of [/深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令 and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击/, /反馈的人太多了/, /public issue #4/, /PR #12/, /never by the skill id alone/, /`rawRule` keeps the official row/]) assert.match(sub, re);
|
||||
// the normative lines and the settled decision
|
||||
assert.match(DESIGN, /every MANUAL 重装 skill, §21\.12 — but the six of the deliberate deviation, §21\.29/);
|
||||
assert.match(DESIGN, /\*\*partly reverted by §21\.29\*\*/);
|
||||
assert.match(DESIGN, /\| Six 重装 skills' trigger \(§21\.29\) \|/);
|
||||
// the builder's table is per chess (灰毫 S1 is the generic skcom_atk_up[3]) and the data follows it
|
||||
const bd = doc('tools/build-data.mjs');
|
||||
assert.match(bd, /const TRIGGER_DEVIATIONS = Object\.freeze\(\{/);
|
||||
for (const k of ["chess_char_1_04_a: { skchr_udflow_2: 'DEFAULT' }", "chess_char_1_20_a: { skchr_liskam_2: 'DEFAULT' }", "chess_char_2_18_a: { 'skcom_atk_up[3]': 'DEFAULT', skchr_ashlok_2: 'DEFAULT' }", "chess_char_5_08_a: { skchr_horn_2: 'DEFAULT', skchr_horn_3: 'DEFAULT' }"]) assert.ok(bd.includes(k), k);
|
||||
for (const id of ['chess_char_1_04_b', 'chess_char_2_18_b', 'chess_char_5_08_b']) {
|
||||
for (const s of DATA.chess[id].skills.filter((x) => x.trigger.rawRule === 'TAKE_DAMAGE' && x.trigger.rule === 'DEFAULT')) assert.ok(bd.includes(s.skillId), `${id} ${s.skillId}`);
|
||||
}
|
||||
// research 03 §1.4: the community summary is gone (promised on PR #12); the activity_table history and the deviation stay
|
||||
const r03 = doc('docs/research/03-operators.md');
|
||||
assert.ok(!/arknights\.wiki\.gg/.test(r03), 'research 03: no wiki.gg summary');
|
||||
assert.ok(!/Offensive skills activate when an enemy/.test(r03));
|
||||
assert.match(r03, /上半 \(act1autochess, 2025-11\) shipped no TANK row/);
|
||||
assert.match(r03, /下半 \(act2autochess, 2026-03-14\) added `TANK \| \| \| 0 \| TAKE_DAMAGE` for every skill index/);
|
||||
assert.match(r03, /\*\*Deliberate deviation\*\* \(the owner, 2026-10-03/);
|
||||
// PR #12's kit lines stay; their comments give this reason, not the community summary
|
||||
const t1 = doc('server/sim/content/kits/tier1.js');
|
||||
assert.equal((t1.match(/trigger: 'DEFAULT',/g) || []).length, 2, "PR #12's two kit lines");
|
||||
assert.ok(!/offensive skills activate when an enemy is in their skill range/.test(t1));
|
||||
assert.ok(!/documented for skillIndex 0/.test(t1));
|
||||
assert.match(DATA_MD, /a deliberate deviation, `tools\/build-data\.mjs TRIGGER_DEVIATIONS`, DESIGN §21\.29/);
|
||||
assert.match(SIM, /the six of DESIGN §21\.29/);
|
||||
assert.match(PLAYING, /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放/);
|
||||
assert.match(doc('CHANGELOG.md'), /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能改为攻击范围内有敌人时就释放/);
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
// The owner's decision after 0.1.1's community feedback (2026-10-03, "反馈的人太多了"; GitHub issue #4, PR #12): six 重装
|
||||
// skills cast with an enemy in range — the basic strategy, DEFAULT — instead of the official 下半 TANK class row's
|
||||
// TAKE_DAMAGE. A deliberate, documented deviation (DESIGN §21.29): 深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令
|
||||
// and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击. It lives in the data (tools/build-data.mjs TRIGGER_DEVIATIONS,
|
||||
// keyed per chess: 灰毫 S1 is the generic skcom_atk_up[3] other chess carry too), `rawRule` keeps the official TAKE_DAMAGE.
|
||||
// Every other MANUAL 重装 skill — 深巡 S1 侵袭破坏应对 among them — still waits for a hit.
|
||||
//
|
||||
// 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
|
||||
// objective; the operator stands on (9,5) facing the gate with its skill fully charged.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { makeBattle, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
import { bodyInKeys } from '../../server/sim/body.js';
|
||||
|
||||
const CHESS = JSON.parse(fs.readFileSync(new URL('../../data/chess.json', import.meta.url), 'utf8'));
|
||||
const ds = getDefaultSource();
|
||||
|
||||
/** [normal chess id, skill id] — both records (normal + elite) of each chess. */
|
||||
const DEVIATED = [
|
||||
['chess_char_1_04_a', 'skchr_udflow_2'], // 深巡 S2 行动能力剥夺
|
||||
['chess_char_1_20_a', 'skchr_liskam_2'], // 雷蛇 S2 反击电弧
|
||||
['chess_char_5_08_a', 'skchr_horn_2'], // 号角 S2 暴风号令
|
||||
['chess_char_5_08_a', 'skchr_horn_3'], // 号角 S3 终极防线
|
||||
['chess_char_2_18_a', 'skcom_atk_up[3]'], // 灰毫 S1 攻击力强化·γ型
|
||||
['chess_char_2_18_a', 'skchr_ashlok_2'], // 灰毫 S2 专注轰击
|
||||
];
|
||||
const both = (normalId) => [normalId, CHESS[normalId].goldenId];
|
||||
const isDeviated = (c, skillId) => DEVIATED.some(([id, s]) => id === c.baseId && s === skillId);
|
||||
const ENEMY = 'enemy_1422_lrsldr';
|
||||
|
||||
/**
|
||||
* One real battle: `chessId` with skill `skillId` on (9,5) facing RIGHT, SP full; one 萨卡兹枯朽前锋 on the lane from
|
||||
* `spawnAt` s. Returns the first cast (time, reason, whether an enemy stood on the unit's initial range then, whether
|
||||
* the unit blocked it) and the time the unit first took damage (any `damaged` event: what TAKE_DAMAGE listens to).
|
||||
*/
|
||||
function firstCast(chessId, skillId, { spawnAt = 0, seconds = 40 } = {}) {
|
||||
const index = CHESS[chessId].skills.find((s) => s.skillId === skillId).index;
|
||||
const h = makeBattle({
|
||||
stageId: 'act2autochess_m01', seed: 3, timeLimit: 120, autoFinish: false,
|
||||
units: [{ chessId, row: 9, col: 5, dir: 'RIGHT', skillIndex: index, carryState: { sp: 999 } }],
|
||||
enemies: [{ key: ENEMY, route: 0, time: spawnAt }],
|
||||
});
|
||||
const u = h.unit(chessId);
|
||||
assert.equal(u.skill.id, skillId, `${chessId} runs ${skillId}`);
|
||||
const out = { rule: u.skill.rule, cast: null, reason: null, enemyInRange: false, blocking: false, hurt: null };
|
||||
h.b.on('damaged', (ctx) => { if (ctx.target === u && out.hurt == null) out.hurt = h.b.time; }, { priority: 1000 });
|
||||
h.b.on('skillStart', (ctx) => {
|
||||
if (ctx.unit !== u || out.cast != null) return;
|
||||
out.cast = h.b.time;
|
||||
out.reason = ctx.reason;
|
||||
const keys = new Set(u.baseRangeKeys || u.rangeKeys);
|
||||
out.enemyInRange = h.b.enemies.some((e) => e.alive && e.deployed && bodyInKeys(e, keys));
|
||||
out.blocking = u.blocking.length > 0;
|
||||
}, { priority: 1000 });
|
||||
h.runUntil(() => out.cast != null && out.hurt != null, seconds);
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
return out;
|
||||
}
|
||||
|
||||
test('data: the six deviated 重装 skills are DEFAULT 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}`);
|
||||
// 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`);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
assert.equal(n, 12);
|
||||
// nothing else changed: every other MANUAL 重装 skill keeps the row (深巡 S1 included), and the generic
|
||||
// skcom_atk_up[3] keeps each other carrier's own rule (the deviation is keyed per chess, never per skill id)
|
||||
for (const c of Object.values(CHESS)) {
|
||||
for (const s of c.skills || []) {
|
||||
if (isDeviated(c, s.skillId)) continue;
|
||||
if (s.trigger.rawRule === 'TAKE_DAMAGE') assert.equal(s.trigger.rule, 'TAKE_DAMAGE', `${c.chessId} ${s.skillId}`);
|
||||
if (c.profession === 'TANK' && s.skillType === 'MANUAL' && s.trigger.rawRule !== 'CUSTOM_RANGE_SEARCH_ENEMY') assert.equal(s.trigger.rule, 'TAKE_DAMAGE', `${c.chessId} ${s.skillId}`);
|
||||
}
|
||||
}
|
||||
for (const id of both('chess_char_1_04_a')) assert.equal(CHESS[id].skills.find((s) => s.skillId === 'skchr_udflow_1').trigger.rule, 'TAKE_DAMAGE', `${id} 深巡 S1`);
|
||||
const others = Object.values(CHESS).filter((c) => c.baseId !== 'chess_char_2_18_a' && c.skills?.some((s) => s.skillId === 'skcom_atk_up[3]'));
|
||||
assert.ok(others.length >= 10, 'skcom_atk_up[3] is shared');
|
||||
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 four deviated skills read the data', async () => {
|
||||
const { KITS } = await import('../../server/sim/content/index.js');
|
||||
for (const [normalId, skillId] of DEVIATED) {
|
||||
for (const id of both(normalId)) {
|
||||
const rec = CHESS[id];
|
||||
const s = rec.skills.find((x) => x.skillId === skillId);
|
||||
const def = ds.getChess(id, { skillIndex: s.index });
|
||||
const kit = KITS[rec.baseId]?.(def.skill.bb, def.raw, def);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('real battle: each deviated skill casts with an enemy in range before anything hits the operator (normal + elite)', () => {
|
||||
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);
|
||||
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.ok(r.hurt == null || r.hurt > r.cast, `cast before the first hit — ${where}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('real battle: 深巡 S1 and every other MANUAL 重装 skill still wait for a hit (TAKE_DAMAGE)', () => {
|
||||
let n = 0;
|
||||
for (const c of Object.values(CHESS)) {
|
||||
if (c.profession !== 'TANK' || c.isDiy) continue;
|
||||
for (const s of c.skills) {
|
||||
if (s.skillType !== 'MANUAL' || s.trigger.rule !== 'TAKE_DAMAGE') continue;
|
||||
const r = firstCast(c.chessId, s.skillId);
|
||||
const where = `${c.chessId} ${c.name} ${s.skillId}: ${JSON.stringify(r)}`;
|
||||
assert.equal(r.rule, 'TAKE_DAMAGE', where);
|
||||
assert.ok(r.hurt != null && r.cast != null, `hit and cast — ${where}`);
|
||||
assert.equal(r.reason, 'TAKE_DAMAGE', where);
|
||||
assert.equal(r.cast, r.hurt, `cast by the first hit, not before — ${where}`);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
assert.ok(n >= 40, `${n} TAKE_DAMAGE records checked`);
|
||||
const udflowS1 = firstCast('chess_char_1_04_a', 'skchr_udflow_1');
|
||||
assert.ok(udflowS1.cast > 10, `深巡 S1 waits for the enemy to reach and hit her: ${JSON.stringify(udflowS1)}`);
|
||||
});
|
||||
+41
-5
@@ -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`);
|
||||
|
||||
Reference in new issue
Block a user