feedback1 WE: 鸭爵 strategy enemies follow the official rules

- 圆仔 is unblockable (PRTS 天赋 "无法攻击/被阻挡"); it was blocked like any
  ground enemy because its kit only had the front guard.
- 鸭爵 runs at move speed x(1 + run.attack@move_speed) once hurt: +300 % for
  the strategy version, +400 % for the original (PRTS); it was x3 / x4.
- The swap also applies in the Final Assault and the Hidden Core (PRTS
  下半/PRTS盟约记录 鸭爵 备注 "在最终回合和隐秘核心回合中仍然生效"): any
  ground wave enemy but the leader, its parts, bounties and uncounted units
  can be swapped (the leader rounds' escorts are elites); each player of a
  boss field gets its own 0-2.
- The four swapped-in enemies cost 1 LP at the protection point
  ("但进入保护目标点将减少1点目标生命值"): build-data sets their lpr to 1
  instead of the database's roguelike 0 (Final Assault LP, detail card).
This commit is contained in:
sganggs committed 2026-10-03 01:45:54 +08:00
1 parent 7f134737ce
commit f1e7f7da1a
10 files changed
+262 -19

No files matched your search

+1 -1
View File
@@ -1 +1 @@
{"enemy_1000_gopro_2":{"key":"enemy_1000_gopro_2","name":"猎狗pro","level":0,"rank":"NORMAL","handbookIndex":"O2","desc":"比一般猎狗更具有作战能力,行动速度很快。","descRaw":"比一般猎狗更具有作战能力,<@eb.key>行动速度很快</>。","applyWay":"MELEE","stats":{"maxHp":1700,"atk":260,"def":0,"res":20,"moveSpeed":1.9,"bat":1.4,"aspd":100,"rangeRadius":0,"rawRangeRadius":0,"blockCnt":1,"massLevel":0,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys"],"motion":"WALK","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":["infection"],"be":3060,"beFactor":1,"attrPower":3060,"isFlyEnemy":false,"tokenOnly":false,"acTypes":["SPECIAL"],"acType":"SPECIAL","templateSlot":"T","summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1000_gopro_2","spine":"enemy_1000_gopro_2","modelScale":0.963},"enemy_1001_bigbo":{"key":"enemy_1001_bigbo","name":"大鲍勃","level":0,"rank":"BOSS","handbookIndex":"11","desc":"整合运动的特殊近战人员,使用电锯进行攻击,兼具高耐久、攻击力和防御力。","descRaw":"整合运动的特殊近战人员,使用电锯进行攻击,兼具<@eb.danger>高耐久、攻击力和防御力</>。","applyWay":"MELEE","stats":{"maxHp":22000,"atk":1900,"def":800,"res":30,"moveSpeed":0.5,"bat":6,"aspd":100,"rangeRadius":0,"rawRangeRadius":0,"blockCnt":1,"massLevel":4,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys"],"motion":"WALK","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":[],"be":33990,"beFactor":1,"attrPower":33990,"isFlyEnemy":false,"tokenOnly":false,"acTypes":[],"acType":null,"templateSlot":null,"summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1001_bigbo","spine":"enemy_1001_bigbo","modelScale":1.1111},"enemy_1005_yokai":{"key":"enemy_1005_yokai","name":"妖怪","level":0,"rank":"NORMAL","handbookIndex":"D1","desc":"【飞行单位】敌方人员操纵的无人机,不会进行攻击。","descRaw":"<@eb.key>【飞行单位】</>敌方人员操纵的无人机,不会进行攻击。","applyWay":"NONE","stats":{"maxHp":800,"atk":0,"def":50,"res":0,"moveSpeed":0.9,"bat":2.3,"aspd":100,"rangeRadius":0,"rawRangeRadius":0,"blockCnt":1,"massLevel":0,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"none","dmgTypes":["none"],"motion":"FLY","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[{"text":"飞行单位","textRaw":"飞行单位","format":"NORMAL"}],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":["drone"],"be":950,"beFactor":1,"attrPower":950,"isFlyEnemy":true,"tokenOnly":false,"acTypes":["FLY"],"acType":"FLY","templateSlot":"NF","summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1005_yokai","spine":"enemy_1005_yokai","modelScale":0.7407},"enemy_1005_yokai_2":{"key":"enemy_1005_yokai_2","name":"妖怪MKII","level":0,"rank":"NORMAL","handbookIndex":"D2","desc":"【飞行单位】妖怪的改进版,可进行远程物理射击。","descRaw":"<@eb.key>【飞行单位】</>妖怪的改进版,可进行<@eb.key>远程物理射击</>。","applyWay":"RANGED","stats":{"maxHp":1550,"atk":220,"def":50,"res":0,"moveSpeed":0.9,"bat":3,"aspd":100,"rangeRadius":2,"rawRangeRadius":2,"blockCnt":1,"massLevel":0,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys"],"motion":"FLY","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[{"text":"飞行单位","textRaw":"飞行单位","format":"NORMAL"}],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":["drone"],"be":4000,"beFactor":0.7,"attrPower":2800,"isFlyEnemy":true,"tokenOnly":false,"acTypes":["FLY"],"acType":"FLY","templateSlot":null,"summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1005_yokai_2","spine":"enemy_1005_yokai_2","modelScale":0.8148},"enemy_1005_yokai_3":{"key":"enemy_1005_yokai_3","name":"威龙","level":0,"rank":"ELITE","handbookIndex":"D3","desc":"【飞行单位】大型无人机,拥有更好的耐久性能,可进行远程物理射击。","descRaw":"<@eb.key>【飞行单位】</>大型无人机,拥有更好的耐久性能,可进行<@eb.key>远程物理射击</>。","applyWay":"RANGED","stats":{"maxHp":30000,"atk":660,"def":170,"res":0,"moveSpeed":0.5,"bat":3.8,"aspd":100,"rangLine truncated
{"enemy_1000_gopro_2":{"key":"enemy_1000_gopro_2","name":"猎狗pro","level":0,"rank":"NORMAL","handbookIndex":"O2","desc":"比一般猎狗更具有作战能力,行动速度很快。","descRaw":"比一般猎狗更具有作战能力,<@eb.key>行动速度很快</>。","applyWay":"MELEE","stats":{"maxHp":1700,"atk":260,"def":0,"res":20,"moveSpeed":1.9,"bat":1.4,"aspd":100,"rangeRadius":0,"rawRangeRadius":0,"blockCnt":1,"massLevel":0,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys"],"motion":"WALK","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":["infection"],"be":3060,"beFactor":1,"attrPower":3060,"isFlyEnemy":false,"tokenOnly":false,"acTypes":["SPECIAL"],"acType":"SPECIAL","templateSlot":"T","summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1000_gopro_2","spine":"enemy_1000_gopro_2","modelScale":0.963},"enemy_1001_bigbo":{"key":"enemy_1001_bigbo","name":"大鲍勃","level":0,"rank":"BOSS","handbookIndex":"11","desc":"整合运动的特殊近战人员,使用电锯进行攻击,兼具高耐久、攻击力和防御力。","descRaw":"整合运动的特殊近战人员,使用电锯进行攻击,兼具<@eb.danger>高耐久、攻击力和防御力</>。","applyWay":"MELEE","stats":{"maxHp":22000,"atk":1900,"def":800,"res":30,"moveSpeed":0.5,"bat":6,"aspd":100,"rangeRadius":0,"rawRangeRadius":0,"blockCnt":1,"massLevel":4,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys"],"motion":"WALK","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":[],"be":33990,"beFactor":1,"attrPower":33990,"isFlyEnemy":false,"tokenOnly":false,"acTypes":[],"acType":null,"templateSlot":null,"summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1001_bigbo","spine":"enemy_1001_bigbo","modelScale":1.1111},"enemy_1005_yokai":{"key":"enemy_1005_yokai","name":"妖怪","level":0,"rank":"NORMAL","handbookIndex":"D1","desc":"【飞行单位】敌方人员操纵的无人机,不会进行攻击。","descRaw":"<@eb.key>【飞行单位】</>敌方人员操纵的无人机,不会进行攻击。","applyWay":"NONE","stats":{"maxHp":800,"atk":0,"def":50,"res":0,"moveSpeed":0.9,"bat":2.3,"aspd":100,"rangeRadius":0,"rawRangeRadius":0,"blockCnt":1,"massLevel":0,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"none","dmgTypes":["none"],"motion":"FLY","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[{"text":"飞行单位","textRaw":"飞行单位","format":"NORMAL"}],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":["drone"],"be":950,"beFactor":1,"attrPower":950,"isFlyEnemy":true,"tokenOnly":false,"acTypes":["FLY"],"acType":"FLY","templateSlot":"NF","summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1005_yokai","spine":"enemy_1005_yokai","modelScale":0.7407},"enemy_1005_yokai_2":{"key":"enemy_1005_yokai_2","name":"妖怪MKII","level":0,"rank":"NORMAL","handbookIndex":"D2","desc":"【飞行单位】妖怪的改进版,可进行远程物理射击。","descRaw":"<@eb.key>【飞行单位】</>妖怪的改进版,可进行<@eb.key>远程物理射击</>。","applyWay":"RANGED","stats":{"maxHp":1550,"atk":220,"def":50,"res":0,"moveSpeed":0.9,"bat":3,"aspd":100,"rangeRadius":2,"rawRangeRadius":2,"blockCnt":1,"massLevel":0,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys"],"motion":"FLY","immunities":{"stun":false,"silence":false,"sleep":false,"frozen":false,"levitate":false},"otherImmunities":[],"tauntLevel":0},"abilities":[{"text":"飞行单位","textRaw":"飞行单位","format":"NORMAL"}],"talents":{"bb":{},"bbStr":{}},"skills":[],"notCountInTotal":false,"tags":["drone"],"be":4000,"beFactor":0.7,"attrPower":2800,"isFlyEnemy":true,"tokenOnly":false,"acTypes":["FLY"],"acType":"FLY","templateSlot":null,"summons":[],"inactiveIn":[],"seasonOverride":null,"iconId":"enemy_1005_yokai_2","spine":"enemy_1005_yokai_2","modelScale":0.8148},"enemy_1005_yokai_3":{"key":"enemy_1005_yokai_3","name":"威龙","level":0,"rank":"ELITE","handbookIndex":"D3","desc":"【飞行单位】大型无人机,拥有更好的耐久性能,可进行远程物理射击。","descRaw":"<@eb.key>【飞行单位】</>大型无人机,拥有更好的耐久性能,可进行<@eb.key>远程物理射击</>。","applyWay":"RANGED","stats":{"maxHp":30000,"atk":660,"def":170,"res":0,"moveSpeed":0.5,"bat":3.8,"aspd":100,"rangLine truncated
+1 -1
View File
@@ -335,7 +335,7 @@ blackboards (transitively). Level = `randomEnemyAttributeDict[key].level` (0 for
|---|---|---|
| `key`, `name`, `level`, `rank`, `handbookIndex` | …, `"萨卡兹枯朽前锋"`, `0`, `"NORMAL"`, `"NAC18"` | `rank` NORMAL / ELITE / BOSS |
| `desc`, `descRaw`, `applyWay` | …, `"MELEE"` | `applyWay` MELEE / RANGED / ALL / NONE |
| `stats` | `{"maxHp":4700,"atk":300,"def":100,"res":40,"moveSpeed":0.8,"bat":3,"aspd":100,"rangeRadius":0,"rawRangeRadius":-1,"blockCnt":1,"massLevel":1,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys","arts"],"motion":"WALK","immunities":{…},"otherImmunities":[],"tauntLevel":0}` | level-0 (unscaled). **`rangeRadius` ≥ 0 is the normal-attack radius under the DESIGN §5.5 contract: `> 0` ⇔ the enemy attacks units it is not blocked by; always 0 for `applyWay` MELEE** (melee enemies only hit their blocker; 粉碎攻坚手/宿主士兵/深池方阵步兵 carry an official 1.2–2.5 used by abilities) and negative sentinels become 0. `rawRangeRadius` = the official value. `blockCnt` = block slots it consumes; `lpr` = lifePointReduce; `dmgType` = first handbook damage type (`phys`/`arts`/`heal`/`none`/…), full list in `dmgTypes`. `elementRes` = 损伤抵抗 (official `epResistance` = EP_RESISTANCE, % off every element gauge fill; 10 on 转译基底·α, 0 on every other enemy here) and `elementDmgRes` = 元素抗性 (`epDamageResistance`, % off 元素伤害 — the bursts' HP damage; 0 on every enemy here — newer event enemies of the full database reach 10–15 on both) — PRTS 元素 / 游戏数据基础 and the enemy pages' 损伤抵抗 / 元素抗性 rows. Undefined database fields (`m_defined:false`) holding the zero value fall back to the defaults `lpr` 1, `bat` 1, `aspd` 100, `moveSpeed` 1 (see §15) |
| `stats` | `{"maxHp":4700,"atk":300,"def":100,"res":40,"moveSpeed":0.8,"bat":3,"aspd":100,"rangeRadius":0,"rawRangeRadius":-1,"blockCnt":1,"massLevel":1,"lpr":1,"hpRecoveryPerSec":0,"elementRes":0,"elementDmgRes":0,"hitRatePhys":0,"hitRateArts":0,"dmgType":"phys","dmgTypes":["phys","arts"],"motion":"WALK","immunities":{…},"otherImmunities":[],"tauntLevel":0}` | level-0 (unscaled). **`rangeRadius` ≥ 0 is the normal-attack radius under the DESIGN §5.5 contract: `> 0` ⇔ the enemy attacks units it is not blocked by; always 0 for `applyWay` MELEE** (melee enemies only hit their blocker; 粉碎攻坚手/宿主士兵/深池方阵步兵 carry an official 1.2–2.5 used by abilities) and negative sentinels become 0. `rawRangeRadius` = the official value. `blockCnt` = block slots it consumes; `lpr` = lifePointReduce (LP cost of a leak; except the 鸭爵 strategy's four swapped-in `_2` enemies — the `round_start_all_player_change_enemy_2` enemylist — which cost 1, not the database's roguelike 0: PRTS 下半/PRTS盟约记录 鸭爵 备注 "进入保护目标点将减少1点目标生命值"; build-data `bandSwapEnemyKeys`); `dmgType` = first handbook damage type (`phys`/`arts`/`heal`/`none`/…), full list in `dmgTypes`. `elementRes` = 损伤抵抗 (official `epResistance` = EP_RESISTANCE, % off every element gauge fill; 10 on 转译基底·α, 0 on every other enemy here) and `elementDmgRes` = 元素抗性 (`epDamageResistance`, % off 元素伤害 — the bursts' HP damage; 0 on every enemy here — newer event enemies of the full database reach 10–15 on both) — PRTS 元素 / 游戏数据基础 and the enemy pages' 损伤抵抗 / 元素抗性 rows. Undefined database fields (`m_defined:false`) holding the zero value fall back to the defaults `lpr` 1, `bat` 1, `aspd` 100, `moveSpeed` 1 (see §15) |
| `abilities[]` | `{"text":"位于源石污染区内时,攻击造成法术伤害","textRaw":"…","format":"NORMAL"}` | handbook ability list |
| `talents` | `{"bb":{"1.hp_ratio":0.2,"1.damage_scale":0.5},"bbStr":{}}` | talentBlackboard |
| `skills[]` | `{"prefabKey":"2","priority":0,"cooldown":50,"initCooldown":0,"spCost":0,"bb":{"hp_ratio":0.02},"bbStr":{"branch_id":"boss_summon_enemy","enemy_key":"enemy_1005_yokai"}}` | `branch_id` → `waves[t].branches` |
+1
View File
@@ -74,6 +74,7 @@
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
- **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。
- **敌人**:远程敌人的射程按官方从干员的碰撞体积(半径 0.25 格)算起,比数据上的攻击范围半径远 0.25 格——例如萨卡兹枯朽战车(射程 2.2)能打到中心 2.45 格以内、站在低地的干员。它的第一下攻击就是「秽蚀轰击」(在目标处留下半径 1.7、持续 10 秒的污染区:低地每秒 50、高台 25 点真实伤害,同名不叠加),被阻挡时普通攻击伤害翻倍。“萨科塔之翼”等血量第一次低于一半时会在原地小范围乱飞 5 秒(官方叫「恐惧」);叙拉古、妮芙等干员造成的恐惧会让敌人朝远离干员的方向逃跑。
- **策略「鸭爵」(“神秘顾客”)**:从第 5 回合起,你和队友的每场作战(**最终攻势和隐秘核心也算**,联防不另外替换)有 0–2 名地面敌人(不含领袖及其部件、悬赏敌人)被替换为特殊版本的鸭爵、高普尼克、流泪小子或圆仔,出现在被替换敌人的时间和路线上;击倒者获得 1 资金(联防中的队友也算),漏进保护目标点照常扣 1 点目标生命值。鸭爵和圆仔**不攻击、无法被阻挡**:鸭爵受到伤害后移动速度 +300%;圆仔始终面向我方干员较多的一侧,来自正面的物理 / 法术伤害 −80%。流泪小子隐匿(被阻挡时才能被选中),攻击使目标晕眩 3.5 秒;高普尼克免疫晕眩、冻结和浮空。
- 右上角 🔍▶▶ 可以查看本回合即将来袭的敌人(预览区),左上角 🔍 查看本局信息(禁用盟约、策略)。
## 5. 联防(同盟)
+20 -7
View File
@@ -19,7 +19,8 @@
// round_start_bond_check_gain_layer 余 round start of `round`: exactly factioncount active bonds → +count1, else each active +count2
// up_shop_add_special_goods {count,choice,pool} 凯瑟琳 every level-up: pick 1 of `count` items of pool (free)
// coin_carry_over {capital,interest,max} 坎诺特 leftover ≥ capital at round start → +min(max, ⌊left/capital⌋×interest) income
// round_start_all_player_change_enemy_2 鸭爵 global: every alive player's normal battle from `round` (own + teammates)
// round_start_all_player_change_enemy_2 鸭爵 global: every alive player's battle from `round` (own + teammates;
// normal, Final Assault, Hidden Core): 0–2 enemies → enemylist
// round_start_activate_char_chess_effect_in_board 铃兰 round start: 获得时 traits of the right-most (then bottom-most) board op with one
// first_sell_char_chess_exchange_char_chess_in_shop 巫恋 first sale of a normal op per round: swapped with a random shop chess (no funds)
// round_start_gain_char_chess_in_shop_every_n_round 松桐 round % n = 0: a random shop chess for free (its slot empties)
@@ -348,13 +349,24 @@ K.preparation_start_add_special_goods_every_n_round = (ps) => ({
});
// ---------------------------------------------------------------------------------------------------------------
// 鸭爵: "你和队友遭遇的部分敌人" — a global handler rewrites the normal-battle spawn list of every alive player while
// the band is held by that player or a teammate. From the `minweight`–`maxweight` share of the wave (time order),
// min..max NORMAL ground enemies are replaced by a random enemylist enemy worth 1 fund to its killer (bounty, also in
// 联防). The originals never exist, so none of their death / kill / leak effects happen.
// 鸭爵 “神秘顾客”: "从第5回合起,你和队友遭遇的部分敌人可能会替换为<鸭爵><高普尼克><流泪小子><圆仔>,击倒这些敌人者获得1资金奖励
// (包括联防阶段)". A global handler rewrites the battle spawn list of every alive player while the band is held by that
// player or a teammate: from round `round`, in the normal battle, the Final Assault and the Hidden Core (PRTS 卫戍协议:盟约
// 下半/PRTS盟约记录 鸭爵 备注 "每回合将有0~2名敌人被替换为上述敌人之一…且在最终回合和隐秘核心回合中仍然生效"; the 联防 phase
// replaces nothing: its enemies are leaks, which keep their bounty). min..max (0–2, uniform [ASSUMED]) of the player's
// ground enemies from the `minweight`–`maxweight` share of the wave (time order) [ASSUMED reading of the two weights]
// — any wave enemy but the leader, its parts, bounties, earlier swaps and uncounted units: normal or elite, as the
// Final Assault's escorts are all elites; flyers stay [ASSUMED: the four are ground units and a FLY action's route is
// a FLY route] — become a random enemylist enemy (the act2 `_2` versions) at the replaced unit's time and route, worth
// DUCK_COINS to its killer (bounty, also in 联防). A boss field's two players edit the field's one list in turn, so each
// gets its own 0–2. The originals never exist, so none of their death / kill / leak effects happen. Reaching the
// protection point costs 1 LP ("但进入保护目标点将减少1点目标生命值": the enemies' data lpr, tools/build-data.mjs).
export const DUCK_BAND = 'band_ducklord';
/** "击倒这些敌人者获得1资金奖励" (the blackboard's `count` is 1 too). */
export const DUCK_COINS = 1;
/** Battle kinds the swap applies to (PRTS: the Final Assault and the Hidden Core too). */
const DUCK_KINDS = new Set(['normal', 'boss', 'hidden']);
function duckParams() {
for (const b of buffsOf(bandRecord(DUCK_BAND))) if (b.key === 'round_start_all_player_change_enemy_2') return b.p;
@@ -383,7 +395,8 @@ export function duckReplace(ctx, spawns, p, ownerPlayerId) {
const s = spawns[f.i];
const e = gd.enemy(s.enemyKey);
const frac = n > 1 ? j / (n - 1) : 0;
return !s.tag && e && e.rank === 'NORMAL' && e.stats?.motion !== 'FLY' && frac >= w0 - 1e-9 && frac <= w1 + 1e-9;
return !s.tag && s.countInTotal !== false && e && e.rank !== 'BOSS' && !e.notCountInTotal && e.stats?.motion !== 'FLY'
&& frac >= w0 - 1e-9 && frac <= w1 + 1e-9;
});
const picked = ctx.rng.shuffle(ok).slice(0, want);
if (!picked.length) return [];
@@ -421,7 +434,7 @@ export function duckReplace(ctx, spawns, p, ownerPlayerId) {
const duckGlobal = {
onBattleStart(ctx, ev) {
if (!ev || ev.kind !== 'normal' || !Array.isArray(ev.spawns)) return;
if (!ev || !DUCK_KINDS.has(ev.kind) || !Array.isArray(ev.spawns)) return;
const p = duckParams();
if (!p || ctx.round < int(p.round, 1)) return;
const held = ctx.bandId() === DUCK_BAND || ctx.teammates().some((t) => t.bandId() === DUCK_BAND);
+11 -4
View File
@@ -516,6 +516,11 @@ const onTerrain = (b, u, terrain) => { const [r, c] = tileOf(u); return b.grid.t
const stealth = () => ({ spawn(b, e) { b.addBuff(e, { key: 'ab:stealth', flags: { stealth: true }, persist: true }); } });
/** 无法被阻挡. */
const unblockable = () => ({ spawn(b, e) { b.addBuff(e, { key: 'ab:unblockable', flags: { unblockable: true }, persist: true }); } });
/**
* "受到伤害时开始奔跑,移动速度+N%" (鸭爵 `run.attack@move_speed`): the first damage it takes ⇒ move speed ×(1 + v) for good —
* PRTS 鸭爵 天赋 "移动速度+400%" for v 4, PRTS 卫戍协议:盟约 下半/PRTS盟约记录 鸭爵 备注 "受伤后移动速度+300%" for the act2 v 3.
*/
const runWhenHit = (v) => ({ taken(c, b, e, a) { if (a.on || !(v > 0)) return; a.on = true; b.addBuff(e, { key: 'ab:duckRun', persist: true, mods: { moveMul: 1 + v } }); } });
/** 频次: maxHp = hits. */
const times = (artsOnly = false) => ({ spawn(b, e) { setHits(e, e.def.maxHp); hitCount(b, e, true, artsOnly); } });
/** "只能被阻挡数大于等于N的单位阻挡". */
@@ -2327,8 +2332,8 @@ export const KITS = Object.freeze({
}],
enemy_1501_demonk: () => [maxTargets(2)], // 萨卡兹百夫长 · attacks 2 targets
enemy_10018_sgrobh: () => [maxTargets(2)], // “独轮车玩具” · attacks 2 targets
enemy_2001_duckmi: (ab) => [unblockable(), { taken(c, b, e, a) { if (a.on) return; a.on = true; b.addBuff(e, { key: 'ab:duckRun', persist: true, mods: { moveMul: T(ab, 'run.attack@move_speed') ?? 1 } }); } }], // 鸭爵 · unblockable; runs once hit
enemy_2001_duckmi_2: (ab) => [unblockable(), { taken(c, b, e, a) { if (a.on) return; a.on = true; b.addBuff(e, { key: 'ab:duckRun', persist: true, mods: { moveMul: T(ab, 'run.attack@move_speed') ?? 1 } }); } }], // 鸭爵 · same
enemy_2001_duckmi: (ab) => [unblockable(), runWhenHit(T(ab, 'run.attack@move_speed') ?? 0)], // 鸭爵 · unblockable, no attack; runs +400 % once hurt
enemy_2001_duckmi_2: (ab) => [unblockable(), runWhenHit(T(ab, 'run.attack@move_speed') ?? 0)], // 鸭爵 (鸭爵 strategy) · same, +300 %
enemy_10159_mntrjn: () => [unblockable()], // 伊利昂的木驮兽 · unblockable (passengers n/a)
enemy_2009_csaudc: kitEp('neural', 'combat.attack@ep_damage_ratio'), // 骇笑看客 · neural on hit
enemy_2010_csdcr: (ab) => [ep('neural', T(ab, 'attack.attack@ep_damage_ratio') ?? 0), { // 绯红歌伶 · neural on hit; every 20 hits taken: global enemy ASPD up
@@ -2468,8 +2473,10 @@ export const KITS = Object.freeze({
enemy_1535_wlfmster: kitWolfLord, // 扎罗 · −30 % damage, 溶血骇惧 ⇒ 远古威慑 rebirth ⇒ ranged double hits
enemy_10081_mpplai: kitTranslator, // 转译基底·α · 4 phys / 4 arts hits or being blocked ⇒ one of three forms
enemy_10144_xdelk_2: kitElk, // 乌顶巨角卢鲁 · 角力对决 charge on its blocker (push fails ⇒ damage + stun)
enemy_2085_skzjxd: (ab) => [frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)], // 圆仔 · faces the bigger crowd, front damage −80 %
enemy_2085_skzjxd_2: (ab) => [frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)], // 圆仔 · same
// 圆仔 · PRTS 天赋 "无法攻击/被阻挡;受到来源于正面的物理和法术伤害-80%;自身始终朝向我方干员数量最多的方向" (no attack: applyWay
// NONE; its 倒走 / 【炫耀】 is "仅用于演出,无额外效果"): unblockable, faces the bigger crowd, front damage −80 %
enemy_2085_skzjxd: (ab) => [unblockable(), frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)],
enemy_2085_skzjxd_2: (ab) => [unblockable(), frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)], // (鸭爵 strategy) same
// 失衡 (pushed / pulled by operators)
enemy_1328_cbjedi: (ab) => [unbalanced((b, e, a, d) => { // 弧光锋卫 · loses HP in proportion to the distance moved while unbalanced
const v = T(ab, 'unbalanced_bleed.damage') ?? 0, iv = T(ab, 'unbalanced_bleed.interval') ?? 1;
+4 -3
View File
@@ -337,7 +337,7 @@ test('坎诺特 利滚利: leftover funds are kept; ≥ 5 left ⇒ +1 at round s
cover('band_cannot');
});
test('鸭爵 “神秘顾客”: from R5, 0–2 normal ground enemies in the last 40 % of every teammate\'s wave become 鸭爵 & co. worth 1 fund to the killer', () => {
test('鸭爵 “神秘顾客”: from R5, 0–2 ground enemies in the last 40 % of every teammate\'s wave become 鸭爵 & co. worth 1 fund to the killer', () => {
const p = DATA.bands.band_ducklord.buffs[0];
const ducks = p.bbStr.enemylist.split(',');
const normalKey = Object.values(DATA.enemies).find((e) => e.rank === 'NORMAL' && e.stats.motion === 'WALK').key;
@@ -360,8 +360,9 @@ test('鸭爵 “神秘顾客”: from R5, 0–2 normal ground enemies in the las
assert.ok(ev.spawns.some((sp) => sp.enemyKey === flyKey), 'flyers are never replaced');
}
assert.deepEqual(Object.keys(counts).map(Number).sort(), [0, 1, 2]);
// before R5, in boss rounds, or when nobody holds the band: nothing
for (const [r, kind, band] of [[4, 'normal', 'band_ducklord'], [5, 'boss', 'band_ducklord'], [5, 'normal', null]]) {
// before R5, in the 联防 phase (its enemies are leaks), or when nobody holds the band: nothing (the Final Assault and
// the Hidden Core swap too: test/match/feedback1-ducklord.test.js)
for (const [r, kind, band] of [[4, 'normal', 'band_ducklord'], [5, 'unite', 'band_ducklord'], [5, 'normal', null]]) {
h.ps('p_1').bandId = band;
m.round = r;
const ev = { input: p0.battleInput({ side: 'L', colOffset: 0 }), kind, round: r, spawns: wave() };
+20 -2
View File
@@ -1088,14 +1088,18 @@ for (const key of ['enemy_1501_demonk', 'enemy_10018_sgrobh']) {
}
for (const key of ['enemy_2001_duckmi', 'enemy_2001_duckmi_2']) {
test(`${nm(key)}: unblockable; runs ×${tb(key, 'run.attack@move_speed')} once hit`, () => {
// PRTS 鸭爵 "移动速度+400%" (run 4); the 鸭爵 strategy's version "受伤后移动速度+300%" (run 3): ×(1 + run)
test(`${nm(key)}: unblockable, no attack; runs ×${1 + tb(key, 'run.attack@move_speed')} once hit`, () => {
const h = arena();
h.step();
const e = put(h, key, [10, 7], { move: true });
const v = e.s.moveSpeed;
assert.ok(e.s.flags.unblockable);
assert.ok(e.profile.noAttack);
h.b.dealDamage(null, e, { amount: 1, type: 'true' });
approx(e.s.moveSpeed, v * tb(key, 'run.attack@move_speed'));
approx(e.s.moveSpeed, v * (1 + tb(key, 'run.attack@move_speed')));
h.b.dealDamage(null, e, { amount: 1, type: 'true' });
approx(e.s.moveSpeed, v * (1 + tb(key, 'run.attack@move_speed')), 1e-6, 'once');
});
}
@@ -1622,6 +1626,20 @@ test(`${nm('enemy_10097_crshd')} / ${nm('enemy_2085_skzjxd')}: physical / arts d
approx(h.b.dealDamage(back, y, { amount: 1000, type: 'true' }), 1000, 1e-6, 'true damage is never reduced');
});
for (const key of ['enemy_2085_skzjxd', 'enemy_2085_skzjxd_2']) {
test(`${nm(key)}: 无法攻击/被阻挡 (PRTS 天赋) — walks through a blocker, never attacks`, () => {
const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 5 }], hooks: ['blocked'] });
h.step();
const e = put(h, key, [10, 7], { move: true, route: { motion: 'WALK', start: [10, 7], end: [10, 2], checkpoints: [] } });
assert.ok(e.s.flags.unblockable);
assert.ok(e.profile.noAttack);
h.runUntil(() => e.x < 4, 60);
assert.ok(e.x < 4, `passed the wall (x ${e.x})`);
assert.equal(h.hooksOf('blocked').length, 0);
assert.equal(e.stats.attacks, 0);
});
}
test(`${nm('enemy_10098_crhro')}: 重生 once after ${tb('enemy_10098_crhro', 'reborn.duration')} s at full HP`, () => {
const h = arena();
h.step();
+6 -1
View File
@@ -686,6 +686,11 @@ test('independent re-derivation of every chess and enemy stat from the raw offic
const db = new Map(raw('levels/enemydata/enemy_database.json').enemies.map((e) => [e.Key, e.Value]));
const ov = new Map((raw('levels/activities/act1autochess/level_autochess_enemy_data.json').enemyDbRefs || [])
.filter((r) => r.overwrittenData).map((r) => [r.id, r.overwrittenData]));
// the 鸭爵 strategy's swapped-in enemies cost 1 LP at the protection point (PRTS 卫戍协议:盟约 下半/PRTS盟约记录 鸭爵 备注
// "但进入保护目标点将减少1点目标生命值"; the database's lifePointReduce 0 is the roguelike rule)
const swapped = new Set(Object.values(act.effectBuffInfoDataDict).flat().filter((b) => b.key === 'round_start_all_player_change_enemy_2')
.flatMap((b) => b.blackboard.filter((kv) => kv.key === 'enemylist').flatMap((kv) => kv.valueStr.split(','))));
assert.equal(swapped.size, 4);
for (const e of Object.values(enemies)) {
const base = db.get(e.key).find((l) => l.level === 0).enemyData;
const o = ov.get(e.key);
@@ -700,7 +705,7 @@ test('independent re-derivation of every chess and enemy stat from the raw offic
maxHp: pick((x) => x.attributes?.maxHp, 0), atk: pick((x) => x.attributes?.atk, 0), def: pick((x) => x.attributes?.def, 0),
res: pick((x) => x.attributes?.magicResistance, 0), moveSpeed: pick((x) => x.attributes?.moveSpeed, 1),
bat: pick((x) => x.attributes?.baseAttackTime, 1), aspd: pick((x) => x.attributes?.attackSpeed, 100),
lpr: pick((x) => x.lifePointReduce, 1), massLevel: pick((x) => x.attributes?.massLevel, 0),
lpr: swapped.has(e.key) ? 1 : pick((x) => x.lifePointReduce, 1), massLevel: pick((x) => x.attributes?.massLevel, 0),
motion: pick((x) => x.motion, 'WALK'),
};
for (const [key, v] of Object.entries(exp)) {
+179
View File
@@ -0,0 +1,179 @@
// Player feedback after 0.1.0, workstream WE (report #7): the 鸭爵 strategy “神秘顾客” and its four enemies —
// "鸭爵策略带来的怪物圆仔错误的能被阻挡,鸭爵带来的其它几个敌人你也都复核下规则,数据对不对".
// Official: the band's enemylist names the act2 versions (enemy_*_2; enemy_database level 0) and PRTS 卫戍协议:盟约 下半/
// PRTS盟约记录 §策略 鸭爵 备注: "每回合将有0~2名敌人被替换为上述敌人之一,且上述敌人为特殊版本,相较于原版更弱(鸭爵 6000 生命,
// 受伤后移动速度+300%;高普尼克 11000 / 攻击 1600;流泪小子 4500,攻击附加3.5秒晕眩;圆仔 13000),但进入保护目标点将减少1点
// 目标生命值,且在最终回合和隐秘核心回合中仍然生效"; PRTS 圆仔 天赋 "无法攻击/被阻挡".
// Real match paths (the match harness in virtual time, real data, the real meta registry) and the real Battle.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { PHASE } from '../../shared/constants.js';
import { DATA, makeMatch, give, legalTileFor } from './harness.js';
import { FakeBattle } from './fakeBattle.js';
import { Battle } from '../../server/sim/Battle.js';
import { buildBossWave } from '../../server/match/waves.js';
const BAND = 'band_ducklord';
const PARAMS = DATA.bands[BAND].buffs.find((b) => b.key === 'round_start_all_player_change_enemy_2');
const DUCKS = PARAMS.bbStr.enemylist.split(',');
const YUANZAI = 'enemy_2085_skzjxd_2';
const DUCK = 'enemy_2001_duckmi_2';
const QUIET = { info() {}, debug() {}, warn() {}, error() {} };
const TANKS = Object.values(DATA.chess).filter((c) => c.visible && !c.isGolden && c.profession === 'TANK' && c.tier <= 2).map((c) => c.chessId).sort();
/** A real Battle of a match's battle options (the match itself runs FakeBattles in these tests). */
const realBattle = (m, opts) => new Battle({ data: m.ds, content: m.battleContent, logger: QUIET, ...opts });
/** Solo match at PREP of round `r` holding the 鸭爵 band (FakeBattle rounds before). */
function soloAt(r, seed) {
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed, fake: true }).start();
h.toPrep(r, { band: BAND });
const ps = h.ps('p_0');
assert.equal(ps.bandId, BAND);
return { h, m: h.m, ps };
}
/** The player's normal-battle options until the band swapped a `key` into the real wave (the real onBattleStart path). */
function optsWith(m, ps, key, tries = 400) {
for (let i = 0; i < tries; i++) {
const opts = m._normalOpts(ps);
const spec = opts.spawns.find((s) => s.enemyKey === key);
if (spec) return { opts, spec };
}
assert.fail(`no ${key} in ${tries} battles`);
}
test('#7 圆仔 brought by 鸭爵 is never blocked: it walks past a board full of defenders (real R6 wave, real Battle)', () => {
const { h, m, ps } = soloAt(6, 7071);
h.setStage('act1autochess_m01');
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
ps.board.clear();
ps.hand.fill(null);
// a defender (3 blocks) on every melee tile of the board: any blockable ground enemy meets one
let n = 0;
for (let k = 0; k < 40; k++) {
const id = TANKS.find((t) => m.pool.has(t) && m.pool.left(t) > 0);
const at = id && legalTileFor(m, ps, id);
if (!at) break;
give(m, ps, id, 'board', at);
n++;
}
assert.ok(n >= 6, `${n} defenders`);
const { opts, spec } = optsWith(m, ps, YUANZAI);
assert.deepEqual(spec.bounty, { coins: 1, ownerPlayerId: 'p_0' }, '1 fund to whoever knocks it out');
// the swapped-in 圆仔 alone, at once (its real route and round multipliers)
const b = realBattle(m, { ...opts, spawns: [{ ...spec, time: 0 }], timeLimit: 600 });
let yz = null;
let blocked = 0;
for (let t = 0; t < 600 * 30 && !b.finished; t++) {
b.step();
yz = yz || b.enemies.find((e) => e.defId === YUANZAI) || null;
if (yz && yz.alive && yz.blockedBy) blocked++;
}
assert.ok(yz, 'spawned');
assert.equal(blocked, 0, 'never blocked (ticks blocked)');
assert.ok(yz.s.flags.unblockable, 'PRTS 圆仔 天赋 "无法攻击/被阻挡"');
assert.equal(yz.stats.attacks, 0, 'never attacks');
const r = b.result();
const leaked = (r.perPlayer.p_0.leaked || []).filter((l) => l.enemyKey === YUANZAI);
assert.ok(leaked.length === 1 || !yz.alive, 'it either leaks or is knocked out on the way');
m.dispose();
});
test('#7 the four 鸭爵 enemies are the act2 versions: PRTS stats, 1 LP when they reach the protection point', () => {
const s = (k) => DATA.enemies[k].stats;
assert.deepEqual(DUCKS.slice().sort(), ['enemy_2001_duckmi_2', 'enemy_2002_bearmi_2', 'enemy_2034_sythef_2', 'enemy_2085_skzjxd_2']);
assert.equal(s('enemy_2001_duckmi_2').maxHp, 6000);
assert.equal(s('enemy_2002_bearmi_2').maxHp, 11000);
assert.equal(s('enemy_2002_bearmi_2').atk, 1600);
assert.equal(s('enemy_2034_sythef_2').maxHp, 4500);
assert.equal(DATA.enemies.enemy_2034_sythef_2.talents.bb['Combat.attack@stun'], 3.5);
assert.equal(s('enemy_2085_skzjxd_2').maxHp, 13000);
// "但进入保护目标点将减少1点目标生命值" (the database's lifePointReduce 0 is the roguelike 宝藏 rule)
for (const k of DUCKS) assert.equal(s(k).lpr, 1, k);
// the original (roguelike) versions keep the database value
for (const k of ['enemy_2001_duckmi', 'enemy_2002_bearmi', 'enemy_2034_sythef', 'enemy_2085_skzjxd']) assert.equal(s(k).lpr, 0, k);
});
test('#7 鸭爵 runs at +300 % move speed once hurt (PRTS: 受伤后移动速度+300%), never attacks, is never blocked', () => {
const { m, ps } = soloAt(5, 7072);
const { opts, spec } = optsWith(m, ps, DUCK);
const b = realBattle(m, { ...opts, spawns: [{ ...spec, time: 0 }], timeLimit: 600 });
let d = null;
for (let t = 0; t < 30 && !d; t++) { b.step(); d = b.enemies.find((e) => e.defId === DUCK) || null; }
assert.ok(d && d.s.flags.unblockable);
const v = d.s.moveSpeed;
b.dealDamage(null, d, { amount: 1, type: 'true' });
const run = DATA.enemies[DUCK].talents.bb['run.attack@move_speed'];
assert.equal(run, 3);
assert.ok(Math.abs(d.s.moveSpeed - v * (1 + run)) < 1e-9, `${d.s.moveSpeed} = ${v} × ${1 + run}`);
assert.ok(d.profile.noAttack);
m.dispose();
});
test('#7 the swap still happens in the Final Assault (escorts, never the leader or its parts), and a leaked swap costs the merged LP 1', () => {
let swapped = 0;
let lpChecked = false;
for (let seed = 1; seed <= 8 && (!swapped || !lpChecked); seed++) {
const h = makeMatch({ mode: 'solo', difficulty: 'FUNNY', seed: 7100 + seed, fake: true, script: (b) => (b.kind === 'boss' ? { bossDps: 1e9 } : {}) }).start();
const m = h.m;
h.drive(() => m.phase === PHASE.PREP && m.round === m.gd.bossRound, { band: BAND });
h.drive(() => m.phase === PHASE.FINAL_ASSAULT, { band: BAND });
const f = FakeBattle.instances.find((b) => b.kind === 'boss');
assert.ok(f, 'boss field');
const ducks = f.opts.spawns.filter((s) => DUCKS.includes(s.enemyKey));
assert.ok(ducks.length <= PARAMS.bb.max, `${ducks.length} swaps`);
assert.ok(f.opts.spawns.some((s) => s.tag === 'boss'), 'the leader stays');
for (const d of ducks) {
assert.equal(d.tag, 'duck');
assert.deepEqual(d.bounty, { coins: 1, ownerPlayerId: 'p_0' });
}
swapped += ducks.length;
if (ducks.length && !lpChecked) {
// the real boss battle (no operators): the swapped enemy walks into the goal and the team pool loses its lpr 1
const pool = { hp: 1e12, maxHp: 1e12, damage() { return 0; } };
const b = realBattle(m, { ...f.opts, sharedBoss: pool, spawns: [{ ...ducks[0], time: 0 }] });
const leaks = [];
b.on('enemyLeak', (ctx) => leaks.push(ctx.enemy));
for (let t = 0; t < 400 * 30 && !leaks.length; t++) b.step();
assert.equal(leaks.length, 1, 'leaked');
assert.equal(leaks[0].lpr, 1);
const lp = m.teamLp;
m._bossLeak(leaks[0]);
assert.equal(m.teamLp, lp - 1);
lpChecked = true;
}
m.dispose();
}
assert.ok(swapped > 0, 'some Final Assault had a swap');
assert.ok(lpChecked);
});
test('#7 the Hidden Core and pair boss fields: each player of the field gets 0–2 swaps among the escorts', () => {
const h = makeMatch({ mode: 'coop', difficulty: 'HARD', humans: 2, seed: 7201, fake: true }).start();
const m = h.m;
h.toPrep(1);
h.ps('p_0').bandId = BAND;
h.ps('p_1').bandId = 'band_bldsk';
const seen = { boss: 0, hidden: 0 };
for (const [kind, round] of [['boss', m.gd.bossRound], ['hidden', m.gd.hiddenRound]]) {
for (let k = 0; k < 40; k++) {
m.round = round;
const wave = buildBossWave(m.gd, m.rngWaves, m.factions, round, { bossId: kind === 'hidden' ? m.hiddenBossId : m.bossId, solo: false });
const spawns = wave.spawns.map((s) => ({ ...s, mods: s.mods ? { ...s.mods } : undefined }));
const before = spawns.reduce((n, s) => n + (s.count || 1), 0);
for (const [j, ps] of [h.ps('p_0'), h.ps('p_1')].entries()) {
const ev = { input: ps.battleInput({ side: j ? 'R' : 'L', colOffset: j ? 8 : 0 }), kind, round, spawns };
m.dispatch(ps, 'onBattleStart', ev);
}
const ducks = spawns.filter((s) => DUCKS.includes(s.enemyKey));
assert.ok(ducks.length <= 2 * PARAMS.bb.max, `${kind}: ${ducks.length}`);
assert.equal(spawns.reduce((n, s) => n + (s.count || 1), 0), before, 'same enemy count');
assert.ok(spawns.filter((s) => s.tag === 'boss').length >= 1);
for (const d of ducks) assert.ok(['p_0', 'p_1'].includes(d.bounty.ownerPlayerId));
seen[kind] += ducks.length;
}
}
assert.ok(seen.boss > 0 && seen.hidden > 0, JSON.stringify(seen));
m.dispose();
});
+19
View File
@@ -1673,12 +1673,30 @@ const MODEL_SCALES = new Map([
const MODEL_SCALE_BY_PREFAB = new Map();
for (const [v, list] of MODEL_SCALES) for (const k of list) MODEL_SCALE_BY_PREFAB.set(`enemy_${k}`, Math.round((v / MODEL_SCALE_STANDARD) * 1e4) / 1e4);
/**
* The 鸭爵 strategy's swapped-in enemies (`round_start_all_player_change_enemy_2` enemylist — the act2 versions, *_2)
* cost BAND_SWAP_LPR at the protection point, not the database's lifePointReduce 0 (the roguelike 宝藏 rule): PRTS
* 卫戍协议:盟约 下半/PRTS盟约记录 §策略 鸭爵 备注 "…但进入保护目标点将减少1点目标生命值,且在最终回合和隐秘核心回合中仍然生效"
* (player feedback after 0.1.0, #7). Normal rounds count leaks whatever their `lpr`; the Final Assault / Hidden Core
* LP and the detail card's 目标价值 read it.
*/
const BAND_SWAP_LPR = 1;
function bandSwapEnemyKeys(act) {
const out = new Set();
for (const buffs of Object.values(act.effectBuffInfoDataDict || {})) for (const b of buffs || []) {
if (b?.key !== 'round_start_all_player_change_enemy_2') continue;
for (const kv of b.blackboard || []) if (kv.key === 'enemylist') for (const k of String(kv.valueStr || '').split(',')) if (k.trim()) out.add(k.trim());
}
return out;
}
/**
* Build data/enemies.json: base stats at the season level (randomEnemyAttributeDict.level, 0 for
* all), with the season-wide override level (level_autochess_enemy_data) applied.
*/
function buildEnemies(ctx) {
const { ac, act, handbook } = ctx;
const swapKeys = bandSwapEnemyKeys(act);
const c = ac.constData;
const hpF = c.enemyMaxHpFactor ?? 1, atkF = c.enemyAtkFactor ?? 5, defF = c.enemyDefFactor ?? 3, resF = c.enemyMagicResistanceFactor ?? 3;
const globalOverrides = new Map();
@@ -1736,6 +1754,7 @@ function buildEnemies(ctx) {
otherImmunities: ['disarmedCombat', 'feared', 'palsy', 'attract', 'teleport', 'groundBound'].filter((k) => !!mv(at[`${k}Immune`], false)),
tauntLevel: mv(at.tauntLevel, 0),
};
if (swapKeys.has(key)) stats.lpr = BAND_SWAP_LPR;
const beFactor = rand?.enemyBattleEffectivenessFactor ?? 1;
const be = beFactor > 0 ? Math.round((stats.maxHp * hpF + stats.atk * atkF + stats.def * defF + stats.res * resF) / beFactor) : null;
const talents = flattenBB(data.talentBlackboard, `enemy ${key} talents`);