feedback2 WH: 余 S2 厚礼上宾 casts with an enemy on its x-1 instead of when hit (the owner's deliberate deviation from the 重装 TAKE_DAMAGE row: TRIGGER_DEVIATIONS SKILL_RANGE, data regenerated, only the two 余 records change)
This commit is contained in:
1 parent
a4cf492160
commit
7362ab5439
10 files changed
+129
-42
No files matched your search
+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
|
||||
+1
-1
@@ -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 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: "全场存在可选目标时释放技能"). |
|
||||
| `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 seven: 深巡 / 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 are `DEFAULT` with `rawRule` `TAKE_DAMAGE` — a deliberate deviation, `tools/build-data.mjs TRIGGER_DEVIATIONS`, DESIGN §21.29 — and 余 S2 is `SKILL_RANGE` with `rawRule` `TAKE_DAMAGE` and `customRangeGrid` its x-1, DESIGN §22.10), `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`.
|
||||
|
||||
+1
-1
@@ -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 秒变成寻仇者、特战术师或幽灵,之后才能被击倒。
|
||||
|
||||
+2
-1
@@ -755,7 +755,8 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
`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 (重装: "不受技能范围影响,受到伤害时释放技能"; in the data every MANUAL 重装 skill but
|
||||
the six of DESIGN §21.29 — 深巡 / 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2 — which are `DEFAULT`, a deliberate deviation);
|
||||
the six of DESIGN §21.29 — 深巡 / 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2 — which are `DEFAULT`, a deliberate deviation, and 余
|
||||
S2 厚礼上宾, `SKILL_RANGE` on its own x-1 since DESIGN §22.10);
|
||||
`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: "不受基础策略影响,在初始攻击范围内存在敌人时释放技能");
|
||||
|
||||
@@ -818,11 +818,12 @@ function yu(bb, chess, def) {
|
||||
// S1 今日做东 (TAKE_DAMAGE, hurt SP): passive taunt +taunt_level; active: HP / DEF +, every attack taken ⇒
|
||||
// ep_damage_ratio × ATK 灼燃损伤 on the attacker (install below)
|
||||
skchr_yu_1: { kind: 'duration', mods: { hpPct: num(bb.max_hp), defPct: num(bb.def) } },
|
||||
// S2 厚礼上宾: atk_scale × ATK arts on every enemy of the skill range + the ground-reachable ones teleported onto his
|
||||
// tile (leaders too, unless 自缚: teleportEnemy); block +block_cnt, HP / ATK +, normal attacks deal arts damage. The
|
||||
// burst hits air units too [ASSUMED: no 对空 note on PRTS]; the teleport takes ground units only ("地面可达目标"). The
|
||||
// 'pull' fx only when someone was teleported (each one also gets its own 'teleport' fx). PRTS's 0.13 s between the
|
||||
// damage and the teleport is not modelled (same tick).
|
||||
// S2 厚礼上宾 (cast with an enemy on its x-1: the data's SKILL_RANGE, a deliberate deviation from the 重装 TAKE_DAMAGE
|
||||
// row — tools/build-data.mjs TRIGGER_DEVIATIONS, DESIGN §22.10): atk_scale × ATK arts on every enemy of the skill range
|
||||
// + the ground-reachable ones teleported onto his tile (leaders too, unless 自缚: teleportEnemy); block +block_cnt, HP /
|
||||
// ATK +, normal attacks deal arts damage. The burst hits air units too [ASSUMED: no 对空 note on PRTS]; the teleport
|
||||
// takes ground units only ("地面可达目标"). The 'pull' fx only when someone was teleported (each one also gets its own
|
||||
// 'teleport' fx). PRTS's 0.13 s between the damage and the teleport is not modelled (same tick).
|
||||
skchr_yu_2: {
|
||||
kind: 'duration',
|
||||
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), blockCnt: num(bb.block_cnt) },
|
||||
|
||||
@@ -261,7 +261,7 @@ test('6_03 余 S1 今日做东: taunt +1 while carried; TAKE_DAMAGE cast, HP/DEF
|
||||
assert.equal(d.unit('chess_char_6_03_a').s.taunt, 0);
|
||||
});
|
||||
|
||||
test('6_03 余 S2 厚礼上宾: cast when hit (重装 TAKE_DAMAGE); atk_scale × ATK arts around him, reachable ground enemies teleported onto his tile; block +2, HP/ATK +, arts attacks', () => {
|
||||
test('6_03 余 S2 厚礼上宾: cast with an enemy on its x-1 (SKILL_RANGE — a deliberate deviation from the 重装 TAKE_DAMAGE, DESIGN §22.10); atk_scale × ATK arts around him, reachable ground enemies teleported onto his tile; block +2, HP/ATK +, arts attacks', () => {
|
||||
for (const id of both('chess_char_6_03')) {
|
||||
const sid = 'skchr_yu_2', bb = bbOf(id, sid);
|
||||
const h = run({
|
||||
@@ -272,6 +272,7 @@ test('6_03 余 S2 厚礼上宾: cast when hit (重装 TAKE_DAMAGE); atk_scale ×
|
||||
const u = h.unit(id);
|
||||
usesSkill(u, sid);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 10));
|
||||
assert.equal(started(h, u)[0].reason, 'SKILL_RANGE');
|
||||
const t0 = started(h, u)[0].t;
|
||||
const burst = dealt(h, u, (c) => c.t === t0 && c.dmg.isSkill && !c.dmg.isAttack);
|
||||
assert.equal(burst.length, 3);
|
||||
@@ -1160,7 +1161,7 @@ test('default-skill-only hooks do not run under an alternate skill (余 / 维娜
|
||||
// 余 S2 active: 闲云隐市 is NOT given to every operator (S3 only)
|
||||
{
|
||||
const h = run({
|
||||
defs: { chess: { o1: plain('o1'), o2: plain('o2'), o3: plain('o3') }, enemies: { e: dummy('e', { atk: 100, bat: 1 }) } }, // (重装: TAKE_DAMAGE)
|
||||
defs: { chess: { o1: plain('o1'), o2: plain('o2'), o3: plain('o3') }, enemies: { e: dummy('e', { atk: 100, bat: 1 }) } }, // (S2: an enemy on x-1)
|
||||
units: [U('chess_char_6_03_a', 'skchr_yu_2', 10, 4, { carryState: READY }), { chessId: 'o1', row: 9, col: 6 }, { chessId: 'o2', row: 11, col: 6 }, { chessId: 'o3', row: 12, col: 6 }],
|
||||
enemies: [{ key: 'e', pos: [10, 4] }],
|
||||
});
|
||||
|
||||
+9
-2
@@ -488,9 +488,16 @@ test('chess: skills[] = every skill unlocked at the status, at the chess skill l
|
||||
assert.deepEqual(rules(id), ['DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['TAKE_DAMAGE', 'TAKE_DAMAGE']);
|
||||
}
|
||||
// 余 S2 厚礼上宾 (DESIGN §22.10, the owner's decision of 2026-10-04): SKILL_RANGE on its own x-1, S1 keeps the row, S3 its
|
||||
// official charId row
|
||||
for (const id of ['chess_char_6_03_a', 'chess_char_6_03_b']) {
|
||||
assert.deepEqual(rules(id), ['TAKE_DAMAGE', 'SKILL_RANGE', 'CUSTOM_RANGE']);
|
||||
assert.deepEqual(raws(id), ['TAKE_DAMAGE', 'TAKE_DAMAGE', 'CUSTOM_RANGE_SEARCH_ENEMY']);
|
||||
assert.deepEqual(chess[id].skills[1].trigger.customRangeGrid, chess[id].skills[1].rangeGrid);
|
||||
}
|
||||
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']);
|
||||
assert.equal(deviated.length, 14, 'exactly the six skills of §21.29 and 余 S2, 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', 'chess_char_6_03_a skchr_yu_2']);
|
||||
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`);
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// 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.
|
||||
// One more since 2026-10-04 (the owner's decision after GitHub issue #32 item 1, DESIGN §22.10): 余 S2 厚礼上宾 casts with an
|
||||
// enemy on its own 技能范围 x-1 — SKILL_RANGE, customRangeGrid its range (his attack range is his own tile, so the basic
|
||||
// strategy could not see an enemy he can pull); `rawRule` keeps TAKE_DAMAGE.
|
||||
//
|
||||
// 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
|
||||
@@ -15,6 +18,7 @@ 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';
|
||||
import { absoluteRangeKeys } from '../../server/sim/targeting.js';
|
||||
|
||||
const CHESS = JSON.parse(fs.readFileSync(new URL('../../data/chess.json', import.meta.url), 'utf8'));
|
||||
const ds = getDefaultSource();
|
||||
@@ -28,8 +32,10 @@ const DEVIATED = [
|
||||
['chess_char_2_18_a', 'skcom_atk_up[3]'], // 灰毫 S1 攻击力强化·γ型
|
||||
['chess_char_2_18_a', 'skchr_ashlok_2'], // 灰毫 S2 专注轰击
|
||||
];
|
||||
/** 余 S2 厚礼上宾: SKILL_RANGE on its own x-1 (DESIGN §22.10). */
|
||||
const SKILL_RANGE_DEVIATED = [['chess_char_6_03_a', 'skchr_yu_2']];
|
||||
const both = (normalId) => [normalId, CHESS[normalId].goldenId];
|
||||
const isDeviated = (c, skillId) => DEVIATED.some(([id, s]) => id === c.baseId && s === skillId);
|
||||
const isDeviated = (c, skillId) => [...DEVIATED, ...SKILL_RANGE_DEVIATED].some(([id, s]) => id === c.baseId && s === skillId);
|
||||
const ENEMY = 'enemy_1422_lrsldr';
|
||||
|
||||
/**
|
||||
@@ -46,7 +52,7 @@ function firstCast(chessId, skillId, { spawnAt = 0, seconds = 40 } = {}) {
|
||||
});
|
||||
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 };
|
||||
const out = { rule: u.skill.rule, cast: null, reason: null, enemyInRange: false, enemyInSkillRange: 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;
|
||||
@@ -54,6 +60,8 @@ function firstCast(chessId, skillId, { spawnAt = 0, seconds = 40 } = {}) {
|
||||
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));
|
||||
const sk = u.skill.triggerGrid ? new Set(absoluteRangeKeys(u.skill.triggerGrid, u.tileR, u.tileC, u.dir, 0)) : null;
|
||||
out.enemyInSkillRange = !!sk && h.b.enemies.some((e) => e.alive && e.deployed && bodyInKeys(e, sk));
|
||||
out.blocking = u.blocking.length > 0;
|
||||
}, { priority: 1000 });
|
||||
h.runUntil(() => out.cast != null && out.hurt != null, seconds);
|
||||
@@ -62,7 +70,7 @@ function firstCast(chessId, skillId, { spawnAt = 0, seconds = 40 } = {}) {
|
||||
return out;
|
||||
}
|
||||
|
||||
test('data: the six deviated 重装 skills are DEFAULT on the normal and the elite record, rawRule the official TAKE_DAMAGE', () => {
|
||||
test('data: the six deviated 重装 skills are DEFAULT and 余 S2 is SKILL_RANGE on its x-1, on the normal and the elite record, rawRule the official TAKE_DAMAGE', () => {
|
||||
let n = 0;
|
||||
for (const [normalId, skillId] of DEVIATED) {
|
||||
for (const id of both(normalId)) {
|
||||
@@ -76,6 +84,17 @@ test('data: the six deviated 重装 skills are DEFAULT on the normal and the eli
|
||||
}
|
||||
}
|
||||
assert.equal(n, 12);
|
||||
for (const [normalId, skillId] of SKILL_RANGE_DEVIATED) {
|
||||
for (const id of both(normalId)) {
|
||||
const s = CHESS[id].skills.find((x) => x.skillId === skillId);
|
||||
assert.equal(CHESS[id].profession, 'TANK');
|
||||
assert.ok(s.rangeGrid?.length, `${id} ${skillId}: a 技能范围 of its own`);
|
||||
assert.deepEqual(s.trigger, { rule: 'SKILL_RANGE', rawRule: 'TAKE_DAMAGE', customRangeGrid: s.rangeGrid }, `${id} ${skillId}`);
|
||||
const def = ds.getChess(id, { skillIndex: s.index });
|
||||
assert.equal(def.skill.trigger.rule, 'SKILL_RANGE', `${id} ${skillId} loadout def`);
|
||||
assert.deepEqual(def.skill.trigger.grid, s.rangeGrid, `${id} ${skillId}: the trigger grid is its x-1`);
|
||||
}
|
||||
}
|
||||
// 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)) {
|
||||
@@ -91,9 +110,9 @@ test('data: the six deviated 重装 skills are DEFAULT on the normal and the eli
|
||||
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 () => {
|
||||
test('PR #12\'s kit lines agree with the data: 深巡 / 雷蛇 S2 specs say DEFAULT, the other deviated skills read the data', async () => {
|
||||
const { KITS } = await import('../../server/sim/content/index.js');
|
||||
for (const [normalId, skillId] of DEVIATED) {
|
||||
for (const [normalId, skillId] of [...DEVIATED, ...SKILL_RANGE_DEVIATED]) {
|
||||
for (const id of both(normalId)) {
|
||||
const rec = CHESS[id];
|
||||
const s = rec.skills.find((x) => x.skillId === skillId);
|
||||
@@ -122,6 +141,22 @@ test('real battle: each deviated skill casts with an enemy in range before anyth
|
||||
}
|
||||
});
|
||||
|
||||
test('real battle: 余 S2 casts once the walker steps onto its x-1, before anything hits him (normal + elite)', () => {
|
||||
for (const [normalId, skillId] of SKILL_RANGE_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, 'SKILL_RANGE', where);
|
||||
assert.ok(r.cast != null && r.cast >= 3, `cast once an enemy is on the field — ${where}`);
|
||||
assert.equal(r.reason, 'SKILL_RANGE', where);
|
||||
// (skillStart comes after the cast's own teleport: the walker already stands on his tile then, but he has not
|
||||
// blocked anyone yet — it was pulled in from x-1, it did not walk in)
|
||||
assert.ok(r.enemyInSkillRange && !r.blocking, `an enemy on x-1, pulled in rather than walked in — ${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)) {
|
||||
|
||||
@@ -252,21 +252,51 @@ test('荒芜拉普兰德 S3 facing UP / LEFT / DOWN plays like facing RIGHT (ene
|
||||
// =====================================================================================================================
|
||||
// item 1 — 余 S2 厚礼上宾
|
||||
|
||||
/** 余 on (10,5) with S2 (`sp` carried in); parked enemies (`[key, row, col, tag?]`). */
|
||||
function yuS2(chessId, foes, sp = 999) {
|
||||
const hold = (r, c) => ({ motion: 'WALK', start: [r, c], end: [9, 2], checkpoints: [{ type: 'WAIT', time: 99 }] });
|
||||
/** 余 on (10,5) with S2 (`sp` carried in); parked enemies (`[key, row, col, tag?, motion?]`). */
|
||||
function yuS2(chessId, foes, sp = 999, defs = undefined) {
|
||||
const hold = (r, c, motion = 'WALK') => ({ motion, start: [r, c], end: [9, 2], checkpoints: [{ type: 'WAIT', time: 99 }] });
|
||||
const h = makeBattle({
|
||||
seed: 5, timeLimit: 120, autoFinish: false,
|
||||
seed: 5, timeLimit: 120, autoFinish: false, defs,
|
||||
units: [{ chessId, row: 10, col: 5, dir: 'RIGHT', skillIndex: 1, carryState: { sp } }],
|
||||
enemies: foes.map(([key, r, c, tag]) => ({ key, route: hold(r, c), time: 0, tag })),
|
||||
enemies: foes.map(([key, r, c, tag, motion]) => ({ key, route: hold(r, c, motion), time: 0, tag })),
|
||||
});
|
||||
h.step(1);
|
||||
return h;
|
||||
}
|
||||
const castOf = (h, u) => h.hooksOf('skillStart').find((c) => c.unit === u);
|
||||
|
||||
test('余 S2 (deliberate deviation, DESIGN §22.10): it fires with a ground enemy two tiles away and nobody hitting him — SKILL_RANGE on x-1 — and the enemy lands on his tile', () => {
|
||||
for (const id of ['chess_char_6_03_a', 'chess_char_6_03_b']) {
|
||||
const h = yuS2(id, [['e', 10, 7]], 999, { enemies: { e: enemyRec({ key: 'e', hp: 1e6, speed: 0, atk: 0 }) } });
|
||||
const yu = h.unit(id);
|
||||
assert.equal(yu.skill.rule, 'SKILL_RANGE', 'the data rule (rawRule TAKE_DAMAGE)');
|
||||
const e = h.b.enemies[0];
|
||||
assert.ok(yu.skill.active, 'cast in the first tick');
|
||||
assert.equal(castOf(h, yu).reason, 'SKILL_RANGE');
|
||||
assert.equal(yu.stats.taken, 0, 'nothing hit him');
|
||||
assert.deepEqual([e.y, e.x], [yu.tileR, yu.tileC], 'pulled onto his tile');
|
||||
h.step(1);
|
||||
assert.equal(e.blockedBy, yu, 'and blocked there');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('余 S2: a ranged enemy hitting him from outside x-1 no longer casts it (it was the official TAKE_DAMAGE); it fires once someone stands on x-1', () => {
|
||||
// 深池暗影术师 (range 2.5) on (9,7): outside x-1 (|Δrow| + |Δcol| = 3) but in reach of him
|
||||
const h = yuS2('chess_char_6_03_a', [['enemy_1168_dumage', 9, 7]]);
|
||||
const yu = h.unit('chess_char_6_03_a');
|
||||
assert.ok(h.runUntil(() => yu.stats.taken > 0, 10), 'it hits him');
|
||||
h.run(3);
|
||||
assert.equal(yu.skill.activations, 0, 'no cast from the hit');
|
||||
h.spawn('enemy_1422_lrsldr', { pos: [10, 6] });
|
||||
h.step(2);
|
||||
assert.equal(yu.skill.activations, 1, 'an enemy steps onto x-1: cast');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('余 S2: a leader inside x-1 is teleported onto his tile like any ground enemy (PRTS 卢西恩 / 假想敌:铳 传送抗性 无); a 自缚 leader stays', () => {
|
||||
for (const id of ['chess_char_6_03_a', 'chess_char_6_03_b']) {
|
||||
// (S2 kept empty until the test's own hit: “萨米的意志”'s 冰凌 strikes 余 at once)
|
||||
// (S2 empty at first: it casts the tick its SP is filled, the three leaders already on x-1)
|
||||
const h = yuS2(id, [['enemy_2016_csphtm', 10, 7, 'boss'], ['enemy_9017_achunt', 11, 6, 'boss'], ['enemy_9033_acdeer', 9, 4, 'boss']], 0);
|
||||
const yu = h.unit(id);
|
||||
assert.ok(!yu.skill.active && !yu.skill.ready);
|
||||
@@ -276,8 +306,8 @@ test('余 S2: a leader inside x-1 is teleported onto his tile like any ground en
|
||||
const samiAt = [sami.x, sami.y];
|
||||
const fx0 = h.eventsOf('fx').length;
|
||||
yu.skill.gainSp(yu.skill.spCost, 'init', true);
|
||||
h.b.dealDamage(gun, yu, { amount: 10, type: 'phys', isAttack: true });
|
||||
assert.ok(yu.skill.active && yu.skill.id === 'skchr_yu_2', 'S2 cast on the hit (TAKE_DAMAGE)');
|
||||
h.step(1);
|
||||
assert.ok(yu.skill.active && yu.skill.id === 'skchr_yu_2' && castOf(h, yu).reason === 'SKILL_RANGE', 'S2 cast (SKILL_RANGE)');
|
||||
for (const e of [lucien, gun]) assert.deepEqual([e.y, e.x], [yu.tileR, yu.tileC], `${e.name} teleported onto 余`);
|
||||
assert.deepEqual([sami.x, sami.y], samiAt, '自缚: not a reachable target (PRTS 余 S2 备注)');
|
||||
const fx = h.eventsOf('fx').slice(fx0);
|
||||
@@ -290,15 +320,15 @@ test('余 S2: a leader inside x-1 is teleported onto his tile like any ground en
|
||||
}
|
||||
});
|
||||
|
||||
test('余 S2 with nobody to pull (the usual first hit: a ranged enemy outside x-1): damage-free cast, no teleport and no pull fx', () => {
|
||||
// 深池暗影术师 (range 2.5) on (9,7) and a 萨卡兹枯朽前锋 on (10,8): both outside x-1 (|Δrow| + |Δcol| = 3)
|
||||
const h = yuS2('chess_char_6_03_a', [['enemy_1168_dumage', 9, 7], ['enemy_1422_lrsldr', 10, 8]]);
|
||||
test('余 S2 with nobody to pull (a flyer on x-1 sets it off): the burst hits it, no teleport and no pull fx', () => {
|
||||
const h = yuS2('chess_char_6_03_a', [['fly', 10, 6, null, 'FLY']], 999, { enemies: { fly: enemyRec({ key: 'fly', hp: 1e6, speed: 0, motion: 'FLY' }) } });
|
||||
const yu = h.unit('chess_char_6_03_a');
|
||||
const fx0 = h.eventsOf('fx').length;
|
||||
assert.ok(h.runUntil(() => yu.skill.active, 10), 'the 深池暗影术师 hits him from outside x-1: S2 (TAKE_DAMAGE)');
|
||||
const fx = h.eventsOf('fx').slice(fx0).filter((e) => e[1] === 'teleport' || e[1] === 'pull');
|
||||
assert.deepEqual(fx, [], 'nobody inside x-1: no teleport, no pull fx');
|
||||
for (const e of h.b.enemies) assert.ok(!(Math.round(e.y) === yu.tileR && Math.round(e.x) === yu.tileC), `${e.name} stays`);
|
||||
const fly = h.b.enemies[0];
|
||||
assert.ok(fly.isFlying && yu.skill.active, 'cast: SKILL_RANGE counts any enemy on x-1');
|
||||
assert.ok(fly.hp < fly.s.maxHp, 'the burst hits air units [ASSUMED]');
|
||||
const fx = h.eventsOf('fx').filter((e) => e[1] === 'teleport' || e[1] === 'pull');
|
||||
assert.deepEqual(fx, [], 'a flyer is no 地面可达目标: no teleport, no pull fx');
|
||||
assert.ok(!(Math.round(fly.y) === yu.tileR && Math.round(fly.x) === yu.tileC), 'it stays');
|
||||
done(h);
|
||||
});
|
||||
|
||||
@@ -313,16 +343,18 @@ test('余 S2 on the real leader rounds: 假想敌:铳 teleported onto 余 walk
|
||||
h.step(1);
|
||||
const yu = h.unit('chess_char_6_03_a');
|
||||
const L = () => h.b.enemies.find((e) => e.defId === key && e.alive);
|
||||
const inX1 = (e) => X1.some(([dr, dc]) => Math.round(e.y) === yu.tileR + dr && Math.round(e.x) === yu.tileC + dc);
|
||||
h.b.on('tick', () => { if (!yu.skill.active && !L()?.mem.yuTest) yu.skill.sp = 0; }); // keep S2 for the test's hit
|
||||
assert.ok(h.runUntil(() => L() && inX1(L()), 90), `${key} walks into x-1`);
|
||||
const inX1At = (y, x) => X1.some(([dr, dc]) => Math.round(y) === yu.tileR + dr && Math.round(x) === yu.tileC + dc);
|
||||
// well inside x-1 (0.1 tiles of margin: the next tick's step cannot take it out before the cast)
|
||||
const deep = (e) => [[0, 0], [0.1, 0], [-0.1, 0], [0, 0.1], [0, -0.1]].every(([a, b]) => inX1At(e.y + a, e.x + b));
|
||||
h.b.on('tick', () => { if (!yu.skill.active && !L()?.mem.yuTest) yu.skill.sp = 0; }); // keep S2 for the leader
|
||||
assert.ok(h.runUntil(() => L() && deep(L()), 90), `${key} walks into x-1`);
|
||||
const e = L();
|
||||
assert.ok(e.isBoss && !e.s.flags.selfBound && e.route.legs.length > 1, 'a patrolling leader');
|
||||
e.mem.yuTest = true;
|
||||
const leg0 = e.route.legIdx;
|
||||
yu.skill.gainSp(yu.skill.spCost, 'init', true);
|
||||
h.b.dealDamage(e, yu, { amount: 10, type: 'phys', isAttack: true });
|
||||
assert.ok(yu.skill.active, 'S2');
|
||||
h.step(1);
|
||||
assert.ok(yu.skill.active, 'S2 (SKILL_RANGE)');
|
||||
assert.deepEqual([e.y, e.x], [yu.tileR, yu.tileC], 'teleported onto 余');
|
||||
assert.equal(e.route.pts, null, 'its path is re-planned from there');
|
||||
if (key === 'enemy_9017_achunt') {
|
||||
|
||||
+13
-3
@@ -434,12 +434,17 @@ const ATTACK_RANGE_CHANGE = /攻击(?:范围|距离)(?:与溅射范围)?(?:扩
|
||||
* 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.
|
||||
* 余 S2 厚礼上宾 joined on 2026-10-04 (the owner's decision after GitHub issue #32 item 1, DESIGN §22.10: players saw it fire
|
||||
* from a ranged hit with nobody to pull). His attack range is his own tile (0-1), so the basic strategy would not see an
|
||||
* enemy he could pull: his S2 takes SKILL_RANGE on its own 技能范围 (x-1) — the official strategy of a MANUAL skill with a
|
||||
* 技能范围 when no class row applies, "仅在技能范围内存在敌人(无视其不可选中)时释放技能" — `customRangeGrid` = that range.
|
||||
*/
|
||||
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 终极防线
|
||||
chess_char_6_03_a: { skchr_yu_2: 'SKILL_RANGE' }, // 余 S2 厚礼上宾 (its x-1)
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -456,7 +461,7 @@ const TRIGGER_DEVIATIONS = Object.freeze({
|
||||
* "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其不可选中)时释放技能", customRangeGrid = the skill range;
|
||||
* - else DEFAULT (the basic strategy: ready + about to attack / heal);
|
||||
* - last, the deliberate deviations (TRIGGER_DEVIATIONS, per chess and skill): `rule` from the table, `rawRule` the
|
||||
* official row.
|
||||
* official row (a SKILL_RANGE deviation takes the skill's own range as `customRangeGrid`).
|
||||
* @param {object} skill record from buildSkill (skillId, skillType, desc, rangeGrid)
|
||||
* @param {{operator?: boolean, chessId?: string}} opts operator = a chess (the 技能范围 strategy is written for 干员;
|
||||
* summons keep DEFAULT); chessId = the chess's NORMAL id (TRIGGER_DEVIATIONS key)
|
||||
@@ -474,6 +479,10 @@ function resolveTrigger(ctx, char, charId, skillIdx, skill, { operator = false,
|
||||
}
|
||||
const rawRule = pick ? pick.skillTriggerType : 'DEFAULT';
|
||||
const deviation = chessId ? TRIGGER_DEVIATIONS[chessId]?.[skill.skillId] : null;
|
||||
if (deviation === 'SKILL_RANGE') {
|
||||
if (!skill.rangeGrid) warn(`trigger deviation ${chessId} ${skill.skillId}: SKILL_RANGE without a 技能范围`);
|
||||
return { rule: deviation, rawRule, customRangeGrid: skill.rangeGrid ? skill.rangeGrid.map((p) => p.slice()) : null };
|
||||
}
|
||||
if (deviation) return { rule: deviation, rawRule, customRangeGrid: null };
|
||||
const rule = TRIGGER_RENAME[rawRule] || rawRule;
|
||||
let customRangeGrid = null;
|
||||
@@ -3091,8 +3100,8 @@ 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
|
||||
// the deliberate trigger deviations (DESIGN §21.29, §22.10) 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}`);
|
||||
@@ -3101,6 +3110,7 @@ function validateAll(f) {
|
||||
const s = (c.skills || []).find((x) => x.skillId === skillId);
|
||||
if (!s) err(`trigger deviation ${c.chessId}: no skill ${skillId}`);
|
||||
else if (s.trigger.rawRule !== 'TAKE_DAMAGE' || s.trigger.rule !== rule) err(`trigger deviation ${c.chessId} ${skillId}: ${s.trigger.rawRule} → ${s.trigger.rule}, expected TAKE_DAMAGE → ${rule}`);
|
||||
else if (rule === 'SKILL_RANGE' && (!s.rangeGrid?.length || JSON.stringify(s.trigger.customRangeGrid) !== JSON.stringify(s.rangeGrid))) err(`trigger deviation ${c.chessId} ${skillId}: SKILL_RANGE needs the skill's own range as customRangeGrid`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user