feedback5: merge feedback4 (583899d) — W4B ported: 阿戈尔's devoured base ATK as a 最终加算 (atkFinal), a fallen marker's marks still resolve, 缇缇 S2 pulses re-enter sleep for her trait (applyStatus reenter in server/sim/battle/status.js; the 缇缇 kit in kits/ops/chess_char_5_02-titi.js; Battle.js / tier5.js stay feedback5's façade and shim); golden: roster-042, bond-egirShip-high, coop2-ABYSS-10-boosted moved, the same three as on feedback4 (bond-egirShip-high identical to it)

This commit is contained in:
sganggs committed 2026-10-06 14:47:13 +08:00
commit c6827a0eaf
20 files changed
+302 -96

No files matched your search

+1 -1
View File
@@ -1 +1 @@
{"yanShip":{"bondId":"yanShip","name":"炎","identifier":1,"isCore":true,"bondType":"SEASON","bondOrder":2,"powerIdList":["yan","sui","lungmen","lee","lgd"],"iconId":"icon_yanShip","activeCount":3,"thresholds":[3,6,9],"maxCount":null,"thresholdTemplate":"count_threshold_upward","countMode":"BOARD","countsHand":false,"countsGoldenOnly":false,"activeType":"BATTLE","isActiveInDeck":false,"noStack":false,"weight":10,"maxInactiveBondCount":-1,"layerMilestones":[],"desc":"【炎】干员攻击力提升(受层数影响)\n<在场6名不同【炎】干员>战斗开始时召唤可自主行动的“炎佑”,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用“祛恶之焰”,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场9名不同【炎】干员>改为召唤2只“炎佑”,攻击力提升至1.5倍,受到的伤害-90%","descRaw":"【炎】干员<@ba.vup>攻击力</>提升<@autochess.gray>(受层数影响)</>\n<在场<@autochess.dgreen>6</>名不同【炎】干员>战斗开始时召唤可自主行动的<@ba.vup>“炎佑”</>,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用<@ba.vup>“祛恶之焰”</>,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场<@autochess.dgreen>9</>名不同【炎】干员>改为召唤2只<@ba.vup>“炎佑”</>,攻击力提升至<@ba.vup>1.5</>倍,受到的伤害<@ba.vup>-90%</>","effectId":"bondeffect_yan","effectName":"炎","effectDesc":"【炎】干员攻击力+{0:0%}(受层数影响)\n<在场6名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%攻击力的灼燃损伤,周围敌人受到20%的元素脆弱,周期性使用“祛恶之焰”\n<在场9名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescRaw":"【炎】干员攻击力<@ba.vup>+{0:0%}</><@ba.acrem>(受层数影响)</>\n<在场<@ba.vup>6</>名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%<@ba.vup>攻击力</>的<@ba.vup>灼燃损伤</>,周围敌人受到20%的<@ba.vup>元素脆弱</>,周期性使用“祛恶之焰”\n<在场<@ba.vup>9</>名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescParams":[{"index":0,"format":"0%","base":"base_atk","perStack":"atk_per_stack"}],"bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"},"buffs":[{"key":"env_gbuff_new","bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"}}],"baseParams":["base_atk"],"perStackParams":["atk_per_stack"],"members":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_15_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"visibleMembers":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"spec":{"tiers":[{"need":"3 distinct 炎 on board","effect":"炎 members (+调和 members) ATK x(1 + 0.23 + 0.009*L)."},{"need":"6 distinct","effect":"At battle start spawn 1 \"炎佑\" (enemy_9012_acloon template, allied flying ranged unit, not blockable, moves on its own). ATK = 0.30 x sum(ATK of 炎 operators on board at battle start), maxHP = 0.30 x sum(maxHP of those operators). Template: DEF 200, RES 0, BAT 2.5 s, ASPD 100, attack range radius 2.0 tiles, magic damage, hits up to 3 targets; every hit adds 灼燃损伤 (burn elemental damage) = 20% of its ATK and applies 元素脆弱 (elemental damage taken x1.2) to enemies within 0.8 tiles of the target. Skill \"祛恶之焰\": cooldown 15 s (first cast 15 s after spawn), channels a flame on one enemy in range dealing 60% ATK magic damage per second to it and to enemies within 1.0 tile, for up to 20 s."},{"need":"9 distinct","effect":"Spawn 2 炎佑 instead; each has ATK x1.5 and takes 90% less damage (damage_resistance 0.9)."}],"formulas":{"atkMultiplier":"1 + 0.23 + 0.009*L"},"caps":"none","layerGain":"Economy-driven: 惊蛰 获得时 +shop level (no activation needed); 小满 休整期结束 +1 per operator gained this rouLine truncated
{"yanShip":{"bondId":"yanShip","name":"炎","identifier":1,"isCore":true,"bondType":"SEASON","bondOrder":2,"powerIdList":["yan","sui","lungmen","lee","lgd"],"iconId":"icon_yanShip","activeCount":3,"thresholds":[3,6,9],"maxCount":null,"thresholdTemplate":"count_threshold_upward","countMode":"BOARD","countsHand":false,"countsGoldenOnly":false,"activeType":"BATTLE","isActiveInDeck":false,"noStack":false,"weight":10,"maxInactiveBondCount":-1,"layerMilestones":[],"desc":"【炎】干员攻击力提升(受层数影响)\n<在场6名不同【炎】干员>战斗开始时召唤可自主行动的“炎佑”,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用“祛恶之焰”,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场9名不同【炎】干员>改为召唤2只“炎佑”,攻击力提升至1.5倍,受到的伤害-90%","descRaw":"【炎】干员<@ba.vup>攻击力</>提升<@autochess.gray>(受层数影响)</>\n<在场<@autochess.dgreen>6</>名不同【炎】干员>战斗开始时召唤可自主行动的<@ba.vup>“炎佑”</>,其攻击力生命值分别为开战时【炎】干员攻击力生命值总和的30%,“炎佑”同时攻击3个目标,可使用<@ba.vup>“祛恶之焰”</>,攻击时附带攻击力一定比例的灼燃损伤且周围敌人受到一定比例的元素脆弱\n<在场<@autochess.dgreen>9</>名不同【炎】干员>改为召唤2只<@ba.vup>“炎佑”</>,攻击力提升至<@ba.vup>1.5</>倍,受到的伤害<@ba.vup>-90%</>","effectId":"bondeffect_yan","effectName":"炎","effectDesc":"【炎】干员攻击力+{0:0%}(受层数影响)\n<在场6名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%攻击力的灼燃损伤,周围敌人受到20%的元素脆弱,周期性使用“祛恶之焰”\n<在场9名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescRaw":"【炎】干员攻击力<@ba.vup>+{0:0%}</><@ba.acrem>(受层数影响)</>\n<在场<@ba.vup>6</>名不同【炎】干员>战斗开始时收集【炎】干员部分属性召唤“炎佑”(其攻击力和生命值为【炎】干员攻击力和生命值总和的30%);“炎佑”同时攻击3个目标,攻击附带20%<@ba.vup>攻击力</>的<@ba.vup>灼燃损伤</>,周围敌人受到20%的<@ba.vup>元素脆弱</>,周期性使用“祛恶之焰”\n<在场<@ba.vup>9</>名不同【炎】干员>改为召唤2只“炎佑”;“炎佑”攻击力提升至1.5倍,受到的伤害-90%","effectDescParams":[{"index":0,"format":"0%","base":"base_atk","perStack":"atk_per_stack"}],"bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"},"buffs":[{"key":"env_gbuff_new","bb":{"base_atk":0.23,"atk_per_stack":0.009,"power_bond_char_cnt":6,"ex_bond_char_cnt":9,"damage_resistance":0.9,"atk":1.5},"bbStr":{"key":"act1autochess_bond_eff_yan"}}],"baseParams":["base_atk"],"perStackParams":["atk_per_stack"],"members":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_15_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"visibleMembers":["chess_char_1_03_a","chess_char_2_04_a","chess_char_3_03_a","chess_char_3_04_a","chess_char_4_17_a","chess_char_5_03_a","chess_char_5_12_a","chess_char_5_23_a","chess_char_6_03_a","chess_char_6_15_a"],"spec":{"tiers":[{"need":"3 distinct 炎 on board","effect":"炎 members (+调和 members) ATK x(1 + 0.23 + 0.009*L)."},{"need":"6 distinct","effect":"At battle start spawn 1 \"炎佑\" (enemy_9012_acloon template, allied flying ranged unit, not blockable, moves on its own). ATK = 0.30 x sum(ATK of 炎 operators on board at battle start), maxHP = 0.30 x sum(maxHP of those operators). Template: DEF 200, RES 0, BAT 2.5 s, ASPD 100, attack range radius 2.0 tiles, magic damage, hits up to 3 targets; every hit adds 灼燃损伤 (burn elemental damage) = 20% of its ATK and applies 元素脆弱 (elemental damage taken x1.2) to enemies within 0.8 tiles of the target. Skill \"祛恶之焰\": cooldown 15 s (first cast 15 s after spawn), channels a flame on one enemy in range dealing 60% ATK magic damage per second to it and to enemies within 1.0 tile, for up to 20 s."},{"need":"9 distinct","effect":"Spawn 2 炎佑 instead; each has ATK x1.5 and takes 90% less damage (damage_resistance 0.9)."}],"formulas":{"atkMultiplier":"1 + 0.23 + 0.009*L"},"caps":"none","layerGain":"Economy-driven: 惊蛰 获得时 +shop level (no activation needed); 小满 休整期结束 +1 per operator gained this rouLine truncated
+84 -6
View File
@@ -242,7 +242,7 @@ b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `bat` (base attack time, s), `blockCnt`, `rangeGrid` (ops) / `rangeRadius` (enemies, 0 = melee), `moveSpeed`, `massLevel`, `weight`, `lpr` (enemies), `dmgType` ('phys'|'arts'|'heal'|'true'), `motion` ('WALK'|'FLY'), `cost`, `respawnTime`, `spRecovery`.
**Aggregation** (`units.js`, recomputed lazily when buffs change):
- `ATK = (base + Σflat) × (1 + Σpct) × Πmul` — same shape for `maxHp`, `def`; the 卫戍 systems' "+X%" ATK / DEF / max HP (盟约, 策略, 装备, 机变 cards, per-layer 特质) are 直接乘算 and go into `Σpct` with the skills' "+X%" (`sim/constants.js DIRECT_BONUS_STACKING 'add'`, content/support `directMods`; PRTS 盟约记录 / 游戏数据基础, §20.10) — "提升至X%" effects (炎佑 ×1.5) and the char_attribute_mul 特质 stay in `Πmul`; `res = clamp((base + Σflat) × Πmul, 0, 100)`; `aspd = clamp(100 + Σaspd + base-100, 20, 600)` (`constants.js ASPD_MIN` 20: PRTS 数值范围 "ATTACK_SPEED 默认下限 20", §20.3).
- `ATK = ((base + Σflat) × (1 + Σpct) + ΣatkFinal) × Πmul` — `atkFinal` is the 最终加算 (阿戈尔's devoured base ATK, §24.7); the same shape without it for `maxHp`, `def`; the 卫戍 systems' "+X%" ATK / DEF / max HP (盟约, 策略, 装备, 机变 cards, per-layer 特质) are 直接乘算 and go into `Σpct` with the skills' "+X%" (`sim/constants.js DIRECT_BONUS_STACKING 'add'`, content/support `directMods`; PRTS 盟约记录 / 游戏数据基础, §20.10) — "提升至X%" effects (炎佑 ×1.5) and the char_attribute_mul 特质 stay in `Πmul`; `res = clamp((base + Σflat) × Πmul, 0, 100)`; `aspd = clamp(100 + Σaspd + base-100, 20, 600)` (`constants.js ASPD_MIN` 20: PRTS 数值范围 "ATTACK_SPEED 默认下限 20", §20.3).
- `interval = bat × (1 + ΣbatPct) × 100 / aspd` (blackboard `base_attack_time` is treated as `batPct`, e.g. −0.3 → 70%).
- Changing `maxHp` keeps the HP **ratio**.
- Stats are rounded only for display; the sim keeps floats.
@@ -253,7 +253,7 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
`Buff = { key, source, duration /*s, Infinity*/, stacks=1, maxStacks=1, refresh: 'replace'|'extend'|'stack', mods, flags, onTick?, onExpire?, tags[] }`
- `mods` keys (all optional, additive within a key): `atkFlat, atkPct, atkMul, defFlat, defPct, defMul, hpFlat, hpPct, hpMul, resFlat, resMul, aspd, batPct, blockCnt, rangeExtend (tiles), moveMul, dmgDealtMul (multiplicative, e.g. 1.2), dmgTakenMul, physTakenMul, artsTakenMul, trueTakenMul, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, dodgePhys, dodgeArts, healingDealtMul, healingTakenMul, spRecoveryFlat, spRecoveryMul, maxTargets (+n), shield (absorbs damage; consumed)`.
- `mods` keys (all optional, additive within a key): `atkFlat, atkPct, atkFinal (最终加算: after the percentages), atkMul, defFlat, defPct, defMul, hpFlat, hpPct, hpMul, resFlat, resMul, aspd, batPct, blockCnt, rangeExtend (tiles), moveMul, dmgDealtMul (multiplicative, e.g. 1.2), dmgTakenMul, physTakenMul, artsTakenMul, trueTakenMul, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, dodgePhys, dodgeArts, healingDealtMul, healingTakenMul, spRecoveryFlat, spRecoveryMul, maxTargets (+n), shield (absorbs damage; consumed)`.
- `flags`: `stun, freeze, sleep, silence (no skills), disarm (no attacks), stealth (untargetable by enemies/ops unless blocked or revealed — an operator's radius area damage skips it too, `Battle.foesInRadius`, PRTS 作战机制 §AOE伤害判定, §21.19, and so do an enemy's area effects and buff auras unless the 隐匿 ally blocks that enemy, `targeting.js areaSelectable` / `auraSelectable`, §22.12; an enemy's 隐匿 returns only 3 s after a block ends, or after its PRTS page's "(解除阻挡N秒后恢复)" — `stealthOff` buffs, `targeting.js enemyStealthed`, §22.8; an enemy's b.snap stealth bit is set only while its 隐匿 is on, §21.4), camou (an ally's 迷彩: like stealth for enemy targeting and on screen, but not 隐匿 for 隐匿 conditions — 叙拉古, 家族徽章; §20.2 — and an enemy's splash and other area effects still hit it, ba.camou "无法躲避溅射类攻击", §22.12), liftoff (an ally's 起飞, 蒂比's skills: it blocks flyers only and has 对地规避 — no ground enemy selects it, so no selected damage or status of one lands, while what selects nobody still does: `ignoreSelect` / 无来源 — 无视无法选择 abilities, direct picks, flying units' blasts, a debuff's ticks; it stays a ground unit on its tile; `targeting.js evadesGround`, §21.22), invulnerable, unblockable, levitate, fear (enemy: cannot attack, unblockable, runs to random tiles of the ±45° fan away from the source; no source or itself as source = inside its own tile — `fear.js`, §20.4), taunt (+aggro), cold, noHeal (no heals from others: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), healFree (禁疗 that stops the unit's own heals too, shown as the status 'healFree': 史尔特尔's 余烬 sets it with noHeal — only an HP-regen attribute and her S3's "无视禁疗" start heal pass, §22.7), isolated (孤立: no friendly selector picks it — no heal, buff, aura or talent of an ally, `Battle.allySelectable`; §20.3)`.
- **Status catalogue** (`buffs.js`, keys used by content): `stun`, `freeze` (stun + res −15), `cold` (aspd −30; a second cold while cold ⇒ freeze 3 s unless immune), `sleep` (stun; ends on damage? no: in AK sleep = untargetable & unable to act; keep AK: untargetable & inactive), `slow` (moveMul), `bind` (moveMul 0), `fragile` (dmgTakenMul), `artsFragile` (artsTakenMul), `silence`, `fear` (the 恐惧 walk, `fear.js`), `burnGauge`/`necrosis`/`neural` element gauges (see damage), `stealth`, `camou` (迷彩, its own keys: 忍冬 S3 `vulpis:camou` until her next cast, 寒芒克洛丝 S1), `reveal`.
- Immunities from enemy data (`stunImmune`, `silenceImmune`, `sleepImmune`, `frozenImmune`, `levitateImmune`) are honoured by `applyStatus`; a unit both `invulnerable` and `untargetable` (a 重生, a 永久无敌 leader part) takes no status from the other side at all (PRTS 无敌 "无法被不同阵营选中", §21.19). Flag `selfBound` = 自缚 (守墓石像's 转换模式, the 自缚 leaders) beside its `noMove`: 余 S2 never teleports such a unit (束缚 sets `noMove` only).
@@ -1555,7 +1555,7 @@ Public GitHub issues filed after the 0.1.1 release (2026-10-03), each triaged ag
- **Seen** (GitHub #33 item 1, "阿戈尔阵营复活机制看起来不会触发"): the 5-tier revive works on its own (5 distinct 阿戈尔, one lethal hit each: the first 3 come back on their tile at full HP, normal and 联防 battles alike), but the usual food chain used it up at t = 0. One row, all facing right, 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现: the battle-start devour knocked out 幽灵鲨, 海霓 and 深巡, the revive brought each straight back (all 3 charges), and 乌尔比安's marks still pending on them knocked them out a second time — down until their 70 / 80 s redeploy, no revive left for the fight (联防 battles alike).
- **Official** (PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 阿戈尔 备注): "标记按付与顺序触发【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; the 5-tier "前3名【阿戈尔】干员首次被击倒时立刻复活" (bonds.json `max_free_respawn_cnt` 3), implemented as "受益者因移动之外的原因退场时下次部署的再部署时间和费用归零". So a devour knock-out is a first knock-out and spends a revive, and the revived member takes no further mark.
- **Cause**: `devour()` skipped a mark only while its target or marker was dead (`!t.alive || !m.alive`); a member revived in the same pass was alive again, so its pending marks fired. 埃芒加德's revives went the same way (it revives in place: the next pending mark knocked the revived unit out again and spent a second charge), and so did 不屈's 立刻重新部署 (the redeployed unit was knocked out again in the same instant, where 不屈 does not fire twice, and stayed down).
- **Now**: when the marks are placed, the pass records each target's deployment (`items/battle.js deploymentOf`, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target is off the field or in another deployment, i.e. was knocked out during the pass — whether the 5-tier revive or 不屈's 立刻重新部署 redeployed it or 埃芒加德 / M3茧甲 revived it in place. The marker side is unchanged since 0.1.0 (PRTS describes only the target): a marker off the field gives no further mark, one knocked out and back in the same pass still gives its marks. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each, every later mark on them is cancelled. (0.1.2 revived all three, spending the 3 revives at t = 0; since 0.1.3 they belong to the first 3 members by position, not to the first knocked out — here 幽灵鲨 comes back and 浊心斯卡蒂 / 乌尔比安 keep theirs for the fight, §23.20.) When nothing brings a devoured unit back during the pass (no 5-tier revive, 不屈 redeploy, 埃芒加德 or M3茧甲), the devour resolves exactly as before.
- **Now**: when the marks are placed, the pass records each target's deployment (`items/battle.js deploymentOf`, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target is off the field or in another deployment, i.e. was knocked out during the pass — whether the 5-tier revive or 不屈's 立刻重新部署 redeployed it or 埃芒加德 / M3茧甲 revived it in place. The marker side is unchanged since 0.1.0 (PRTS describes only the target): a marker off the field gives no further mark (withdrawn in 0.1.4: a marker knocked out during the pass still resolves its marks, §24.7), one knocked out and back in the same pass still gives its marks. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each, every later mark on them is cancelled. (0.1.2 revived all three, spending the 3 revives at t = 0; since 0.1.3 they belong to the first 3 members by position, not to the first knocked out — here 幽灵鲨 comes back and 浊心斯卡蒂 / 乌尔比安 keep theirs for the fight, §23.20.) When nothing brings a devoured unit back during the pass (no 5-tier revive, 不屈 redeploy, 埃芒加德 or M3茧甲), the devour resolves exactly as before.
- **联防 (#33 item 3, not changed)**: the members knocked out twice at t = 0 were still down at the end of the 45–60 s own combat (70 s redeploy), so they entered every 联防 knocked out (forced out, §19.3) and the chain there formed without them. After this fix that is far rarer: with the 5-tier, the members the devour knocks out (up to 3) stand again and enter 联防 down only when the fight knocks them out once more (with 3–4 members no 阿戈尔 revive brings them back, as before). The order of the 联防 forced exit and the battle-start devour is forced exit first (§23.2).
- **乌尔比安 S3 (#33 item 2) — seen**: "乌尔比安3技能结束返回会清空阿戈尔阵营增益,转职突袭后疑似不会返回(未转职时三技能会在原地留下一个图标,转职后无图标)". **Official** (PRTS 乌尔比安 S3 备注; the chess skill is `skchr_ulpia_3`, skill range 6-1 = his own tile and the 6 ahead): the anchor's target is "自身所在地块(仅阻挡敌人时)>技能范围内距离自身最近的包含敌人的地块>技能范围内距离最远的地块"; the 【移动】 goes to the landing tile > the tile beyond it > his own tile, and "若目标地块不为当前地块,会在原地部署一个从不混淆的方向"; "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数". **Cause**: the anchor search started one tile ahead, so while he blocked — typical right after a 突袭 landing next to the enemies — he threw ahead, walked off the enemy he blocked (the blast could miss it — with an enemy one tile ahead its 1.8 radius still reaches the one he blocks) and left the marker. **Now**: while he blocks an enemy the anchor lands on his own tile: the blast (radius 1.8) hits what he blocks, he stays put, no marker, nothing to return from. His own tile is a candidate only while he blocks ("仅阻挡敌人时"): without a blocked enemy the search starts one tile ahead, exactly as in 0.1.1, so a flyer over his tile does not keep him there. Considered and not taken: reading the own tile of the 6-1 range as distance 0 for the second rule. Measured with the triage's batch (8 real stages × 5 round templates × 2 seeds, 突袭 through 变形同构体 + 突袭手雷), before the 突袭 landing change (#51, §22.2) is merged: with 突袭, 56 of 84 casts used to move him off his landing tile, now none does — 82 of the 84 came while he blocked, the other 2 (act2 m03, tile (10,8)) have no standable tile ahead (pre-existing); without 突袭, 64 of 80 casts still move him, as before.
- **乌尔比安 S3 — not reproduced** (the triage, real data): no 阿戈尔 gain is cleared on the return. The devour's base ATK / block (`bond:egir:devour`, e.g. +399 ATK, +1 block), the 阿戈尔 HP and the 血脉的哺养 stacks are there before, during and after the S3 (`Battle.relocate` keeps every buff); what ends is the S3's own +40–60 % HP / +200–230 % ATK. With 突袭 he does return and the marker is drawn, both on the landing tile next to the enemies, not on his board tile (8 stages × 5 round templates × 2 seeds); with this fix a cast while he blocks there leaves no marker at all, as officially. **Deviation, kept**: officially the 【移动】 keeps only the skill progress and the second talent's stacks, so the official game drops the devour gains on the move; the remake keeps them through the 【移动】 and the return.
@@ -2172,7 +2172,7 @@ Decisions of the owner, 2026-10-04: a 联防 devour counts members who enter alr
## 24. Community reports after 0.1.3
Reports after the 0.1.3 release. Each was checked against the official data and PRTS; anything with no source is marked [ASSUMED]. Where each is handled: a skill clip with no Begin and no own Idle plays once (德克萨斯 S2 剑雨) → §24.1; 高台 for every melee chess whose trait reads 「可以放置于远程位」 (GitHub #153, PR #69) → §24.2; the 5-阿戈尔 revives go to the first 3 members knocked out (GitHub #105, #140) → §24.3; 联防 阿戈尔 devours the operator in front whoever owns it (GitHub #140) → §24.4; a special terrain tile explains itself on a tap (GitHub #184, PR #185) → §24.5; 活性源石 was two different materials on the two boards (GitHub #184, PR #185) → §24.6. Merged pull requests keep their own notes where they changed the rules: deploy-timed skills as duration skills (PR #109, §5 skill contract), the teammate's hand / temp / equipment in prep scouting (PR #129), 灵巧's knocked-out aura and the hidden-layer bonds (PR #66, research 02).
Reports after the 0.1.3 release. Each was checked against the official data and PRTS; anything with no source is marked [ASSUMED]. Where each is handled: a skill clip with no Begin and no own Idle plays once (德克萨斯 S2 剑雨) → §24.1; 高台 for every melee chess whose trait reads 「可以放置于远程位」 (GitHub #153, PR #69) → §24.2; the 5-阿戈尔 revives go to the first 3 members knocked out (GitHub #105, #140) → §24.3; 联防 阿戈尔 devours the operator in front whoever owns it (GitHub #140) → §24.4; a special terrain tile explains itself on a tap (GitHub #184, PR #185) → §24.5; 活性源石 was two different materials on the two boards (GitHub #184, PR #185) → §24.6; 阿戈尔's devoured base ATK is a 最终加算 and a fallen marker's marks still resolve (GitHub #165, PR #176) → §24.7; 缇缇 S2's sleep pulses count for her trait (GitHub #162) → §24.8. Merged pull requests keep their own notes where they changed the rules: deploy-timed skills as duration skills (PR #109, §5 skill contract), the teammate's hand / temp / equipment in prep scouting (PR #129), 灵巧's knocked-out aura and the hidden-layer bonds (PR #66, research 02).
### 24.1 A skill clip with no Begin and no own Idle plays once — 德克萨斯 S2 剑雨 (player report) — `render/spine.js SpineActor.setSkill / update` (`SKILL_CLIP_MIN`, `skillClipOnce`, `skillEndPending`)
@@ -2316,7 +2316,8 @@ Reports after the 0.1.3 release. Each was checked against the official data and
- **Now**: `opAt` takes the operator on the front tile whoever owns it: a living one (`S.allyAt` without an owner), else
one lying there since the 联防 start (`egirDownAtStart`: forced out, carry.down). The chain goes on through a marked
阿戈尔 by `S.isMember` — for the player's own operators exactly its members, as before; a teammate's by its own bonds.
Everything else is the solo devour: the marker gains the target's base ATK (atkFlat) and block count; a mark on a
Everything else is the solo devour: the marker gains the target's base ATK (atkFlat when written; a 最终加算,
`atkFinal`, since §24.7) and block count; a mark on a
standing teammate's operator resolves (5000 物理流失 less its DEF, the kill credited to the marker), a mark on a down
one resolves nothing (it stays forced out — §19.3's "still counts as standing for its marking"); the knocked-out
teammate's operator belongs to its owner (their 5-tier slot, 不屈 …). Layers: none in 联防 (`layerGainsEnabled`
@@ -2324,7 +2325,8 @@ Reports after the 0.1.3 release. Each was checked against the official data and
- **Measured / verified**: the 0.1.3 own-board case holds through unite.js's real carry: a 30-s own combat in which the
devour knocks out 幽灵鲨 → 歌蕾蒂娅 → fodder (3 阿戈尔, no revive) hands them to 联防 with `carryState.down`, and
乌尔比安 gets exactly the own combat's atkFlat / blockCnt, with no layers. A teammate's fodder in front gives the same
atkFlat / blockCnt / 物理流失 as an own one and as a solo battle.
atkFlat / blockCnt / 物理流失 as an own one and as a solo battle. (§24.7 moved the ATK gain to `atkFinal`; the tests
compare that key now, with the same outcome.)
- **Geometry** (for the owner): the remake's two 联防 helpers stand on board cols 3–9 (left) and 11–17 (right, colOffset
8) with the road column 10 between them, and the boss halves are cols 3–9 / 11–17 too, so on the current maps no
teammate's operator is ever on an 阿戈尔's front tile at the battle start. The tests place one on col 10 to exercise
@@ -2362,3 +2364,79 @@ Reports after the 0.1.3 release. Each was checked against the official data and
- **With the board art installed** the 2D tile is composed exactly like the 3D one: `DEFAULT_ART_LAYERS.infection` draws the official concrete slab (`board3d/atlas.js concrete`, the same atlas rect) and then the crust as a procedural layer; without the art the cell falls back to `MAT_DRAW.infection` (the procedural ground + the same crust). A second variant (`infection2`, the same art mirrored; `PROC_LAYERS.originium2` for the art path) is picked by tile (`tiles.js variantMat`), so a field of 活性源石 does not repeat one pattern.
- **Not [ASSUMED]** — this is our own art: the palette and the recipe are ours, the tile it dresses and its numbers are the data's (a 机变 card can switch the terrain off, §24.5).
- **Tests**: `test/render/originium-art.test.js` (the couplings: both renderers take the palette from `ORIGINIUM`, the canvas converts it, the recipe's thresholds agree, the 2D base slab is the same atlas rect the 3D board lays its crust on, the infection shader has no per-tile fade while the mire keeps its own, two variants are registered and picked, the pulse tint is the shared one), `test/render/originium-cell.e2e.test.js` (headless Chrome: the cell's veins hit the palette's displayed colour (`linearToHex(ORIGINIUM.vein)`), veins reach the border like the middle (no frame), the veins at opposite edges are alike (the cell joins itself), and the second variant is a different pattern).
### 24.7 阿戈尔: the devoured base ATK is a 最终加算, and a fallen marker's marks still resolve (GitHub #165 points 2–3, PR #176 by @SpiritedAwayCN; the owner's decision of 2026-10-06) — `content/bonds/core.js devour`, `units.js _recalc` (`atkFinal`), `buffs.js ADD_KEYS`
- **Seen**: GitHub #165 point 2 (PR #176 by @SpiritedAwayCN): PRTS makes the devoured base ATK a 最终加算, but the remake
added it before the percentages, so the marker's ATK +% scaled it — 1000 base ATK, a +100 % skill and 2000 devoured
gave 6000 instead of 4000. Point 3: the marks of a marker knocked out earlier in the pass were dropped, while PRTS
cancels only the knocked-out target's pending marks — in A → B → C with B devoured first, C never took B's 5000.
- **Official**: PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 阿戈尔 ※: 「该付与来源获得所有标记单位的基础攻击力(最终加算)和阻挡数」;
PRTS 游戏数据基础 属性基本公式 `A_f = F_t[(A + D_p)(1 + D_t) + F_p]` — the 最终加算 F_p comes after the 直接乘算 D_t and
inside the 最终乘算 F_t. The marks: 「所有【阿戈尔】干员完成标记后,每个标记令目标受到一次5000点物理流失的【吞噬】效果」,
「标记按付与顺序触发【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果」 — only the knocked-out TARGET's
pending marks are cancelled —, and 「【吞噬】的物理流失来源为被付与目标自身;单位被【吞噬】击杀时,击杀来源始终为对应标记的付与来源」.
- **Decision**: the owner's decision of 2026-10-06: both as PRTS says (points 2 and 3 of #165, as PR #176 proposed).
- **Cause**: `devour` put the gained ATK in `atkFlat`, inside `(base + atkFlat) × (1 + atkPct) × atkMul`, so every ATK +%
scaled it; the resolve loop skipped a mark whose marker was neither alive nor down since its own combat
(`!(m.alive || downAtStart(m))`, the rule since 0.1.0 — §22.3's "a marker off the field gives no further mark").
- **Now**: a new additive mod key `atkFinal`, the 最终加算 (no existing channel had that place: 鼓舞 emulates one with a
compensated `atkFlat` refreshed by its aura): `ATK = ((base + ΣatkFlat) × (1 + ΣatkPct) + ΣatkFinal) × ΠatkMul`. The
devour buff is `{ atkFinal, blockCnt }`: 1000 base, +100 %, +2000 devoured → 4000 (0.1.3: 6000); a 最终乘算 (`atkMul`,
e.g. 虚弱) still scales the whole, the devoured part included, as F_t. Every other key keeps its meaning. Each mark
resolves unless its target was knocked out during the pass (or lies down since the 联防 start), whatever became of its
marker: B's mark lands on C after B fell to A's, and a knock-out by it is credited to B (`source: m`). Unchanged
(§24.3 / §24.4): the order of the marks and of their resolution, a target's pending marks cancelled once it is knocked
out (revived or not), the 3 revive slots in knock-out order, the 联防 front-tile devour of any owner's operator, the
down-at-start rules (it marks and resolves its marks, nothing resolves on it), the layers once per devoured unit.
- **Open (point 5)**: PRTS makes the 流失's source the target itself and only the kill source the marker. `Battle.loseHp`
has one source for the hooks and the credit (only `sourceless` splits them), so the `damaged` / `fatal` hooks still
see the marker; a split needs a new `loseHp` option and changes what content and every devour test read from the
hooks — not the small, contained change the owner allowed, so it waits.
- [ASSUMED]: a fallen marker's credited kill fires its kill effects like any credited kill in the engine (海霓's first
kill, 斯卡蒂's count): PRTS names the kill source, not whether a knocked-out operator's traits still answer.
- Docs: DESIGN §5.2 / §5.3 and SIM.md §3 (the formula, the mod key), research 02 §3.6 and its JSON spec (data/bonds.json
`egirShip.spec` regenerated with `build-data --offline`; only `spec.algorithm[2]` / `[3]` changed), the core.js
comments; §24.4's `atkFlat` lines are marked. §22.3's marker line above is withdrawn by this section.
- Golden: `bond-egirShip-high` — the three devourers (elite 深巡 / 幽灵鲨 / 斯卡蒂 on row 9) no longer scale the devoured
base ATK by their ATK +% (2867 / 2593 / 2024 ATK at 1 s instead of 3552 / 3288 / 2274), so its 19 enemies fall later
(74.03 → 89.03 s); `coop2-ABYSS-10-boosted` — from round 8 on ai_0 has 3 more 阿戈尔 and 3 more 奥术 layers (海霓's
first-kill trait, the one effect that adds both, now fires in that battle) and both bots' boss damage moves. The
fallen-marker rule moves no scenario.
- Tests: `test/content/bonds_core.test.js` — new '阿戈尔 devour ATK is a 最终加算 …' (1000 / +100 % / +2000 → 4000, a
直接加算 still scaled, ×0.5 scales the whole), rewritten '… a fallen marker's marks still resolve …' (g2 falls to g1's
mark, its own on the fodder lands: 2 × (5000 − 500)), new '… A → B → C with B devoured first …' (C falls to B's mark,
the kill is B's); all three fail on 0.1.3's code. `feedback3-egir-down`, `feedback4-egir-unite` and
`feedback3-ulpia-move` read `atkFinal` instead of `atkFlat`; `test/match/bosshp.test.js` recomputes ATK with the
最终加算. `facing`, `feedback2-doll`, `sim/core` pass unchanged.
### 24.8 缇缇 S2 封护: every sleep pulse of the ward is a new 沉睡 entry for her trait (GitHub #162; the owner's decision of 2026-10-06) — `kits/tier5.js` (缇缇 `skchr_titi_2` `onTick`), `Battle.applyStatus` (`reenter`)
- **Seen**: GitHub #162: the reporter remembers first-hand that in the official mode 缇缇's trait (garrison_125: 萨尔贡 /
精准 +1 per entry, elite +2, at most 24 / 48 per battle) climbs fast while S2 runs (「攻击范围内有陷入沉睡就开始迅速增加直至
上限」); in the remake an enemy held asleep by S2 counted once.
- **Official**: the trait 「<战斗中>每当范围内有敌人或干员进入沉睡或晕眩时,使已激活的【萨尔贡】【精准】层数+1(每场战斗至多24层)」
(elite +2, 48); S2 「…技能开启时使自身与攻击范围内生命比例最低的干员陷入沉睡直到技能结束,期间持续使自身与该目标周围的敌人陷入
沉睡…」 (12 s / elite 15 s). Neither the data nor PRTS gives the pulse's interval or sleep length.
- **Decision**: the owner's decision of 2026-10-06, following the report: each pulse counts as an entry.
- **Cause**: the pulse re-applies a 0.5 s sleep every 0.25 s (`AURA_DUR` / `AURA_IV`), so an enemy beside a ward never
leaves 沉睡 and `statusApplied.entered` (the target did not carry the status) is true at the first pulse only; the
garrison event counts new entries only.
- **Now**: the pulse calls `applyStatus(enemy, 'sleep', { duration: 0.5, source, reenter })`, a new opt-in that reports
`entered` anyway (the sleep buff refreshes as before): once per enemy and pulse — an enemy beside both wards re-enters
once — and not while a longer sleep from elsewhere holds it (more than 0.5 s left). The garrison rule is untouched: a
refresh elsewhere is still no entry (the 125 test's stun refresh), and the sleeper must be in 缇缇's attack range
(`S.inRange`) — an enemy at her back is slept by every pulse and adds nothing. One enemy in range: one entry per pulse
(the 0.25 s timer runs in 1/30 s ticks: every 8 ticks, 0.27 s), the cap (24 / 48, with the ward's own two operators)
5.63 s into S2. No enemy wakes between pulses (the 0.5 s outlasts the pulse) and no status event is added, so the
fight changes only through the earlier layers. `entered` is read by the two garrison events only (冻结 for 初雪 /
银灰; 沉睡 / 晕眩 by garrison_125_a / _b alone).
- [ASSUMED]: the pulse timing (0.25 s / 0.5 s, unchanged) and that each pulse is an entry (the reporter's memory; no
source gives the official interval); a longer sleep from another source is no re-entry.
- Golden: `roster-042` (缇缇 S2) — her trait's 24 layers now come within her first S2 (26.07–37.03 s) instead of over four
casts up to 176.73 s; the totals are the same (capped), and the earlier 萨尔贡 / 精准 layers end the fight at 166.97 s
instead of 182.03 s with 5 knock-outs instead of 7.
- Tests: `test/content/garrisons_battle.test.js` — new '缇缇 125 + S2 封护 (GitHub #162) …' (the real 缇缇, normal and
elite, S2 cast: an enemy at her back sleeps through 8 s of pulses and adds nothing beyond the ward's two operators; an
enemy in range beside both wards — one entry per pulse — takes the trait to 24 / 48 within 6.5 s; fails on 0.1.3's
code). The 125 test (a stun refresh is not an entry) and `kits_alt_t5`'s S2 test pass unchanged.
+6 -3
View File
@@ -493,7 +493,8 @@ physDealtMul, artsDealtMul, dmgTakenMul, physTakenMul, artsTakenMul, trueTakenMu
elementalTakenMul (元素脆弱: 元素伤害), healingDealtMul, healingTakenMul, atkScaleMul, spRecovery, spCostFlat, redeployMul,
hpRegen, shield, flags{…}`.
Aggregation: `ATK/DEF/maxHp = (base + Σflat) × (1 + Σpct) × Πmul`; `res = clamp((base + ΣresFlat) × ΠresMul, 0, 100)`;
Aggregation: `ATK/DEF/maxHp = (base + Σflat) × (1 + Σpct) × Πmul`, for ATK with the 最终加算 `ΣatkFinal` after the
percentages: `ATK = ((base + ΣatkFlat) × (1 + ΣatkPct) + ΣatkFinal) × ΠatkMul`; `res = clamp((base + ΣresFlat) × ΠresMul, 0, 100)`;
`aspd = clamp(base + Σaspd, 20, 600)` (floor 20: PRTS 数值范围 ATTACK_SPEED 默认下限; user playtest #6); `interval = bat × (1 + ΣbatPct) × 100 / aspd`; `moveSpeed = (base + ΣmoveFlat) × ΠmoveMul`;
tiles/s = `moveSpeed × MOVE_SCALE (0.5)`. A maxHp change keeps the HP ratio. Elite stats (module included) come from data.
@@ -504,7 +505,9 @@ ATK / DEF / max HP bonus of the 卫戍 systems — 盟约, 策略 (bands), 装
`content/support directMods({ atk, def, hp })` (constants.js `DIRECT_BONUS_STACKING` 'add'; 'multiply' = the v2.5
per-source ×(1 + x), which compounded with layers: user report after playtest #6). `Πmul` is for 最终乘算 / "提升至X%"
effects (炎佑 ×1.5 at 9 炎, 虚弱, 停顿 …) and the char_attribute_mul 特质 ("攻击力和生命值+20%", a rune on the base
attributes). Damage multipliers (`dmgDealtMul`, "伤害提升至X%"; `*TakenMul`, 脆弱 / "受到的伤害+X%") multiply each other
attributes). `atkFinal` is the **最终加算** (PRTS `A_f = F_t[(A + D_p)(1 + D_t) + F_p]`: added after the 直接乘算, inside
the 最终乘算): 阿戈尔's devoured base ATK ("基础攻击力(最终加算)", DESIGN §24.7) — a skill's ATK +% does not scale it.
Damage multipliers (`dmgDealtMul`, "伤害提升至X%"; `*TakenMul`, 脆弱 / "受到的伤害+X%") multiply each other
(PRTS 游戏数据基础 "同种倍率间叠乘"), same-named statuses keep the strongest — catalogue statuses (§3) and the content
effects routed through `battle.applyStrongest` (§3: 奥术, 灵知 坚冰, 莱恩哈特 / 缄默德克萨斯 RES cuts), also across the two
players of a pair field or two copies of one operator. A content buff keyed per unit (`key:${unit.id}`) still stacks
@@ -526,7 +529,7 @@ mods, flags, onTick(ctx), interval, onExpire(ctx), onRemove(ctx), tags, shield,
damage passes); none = every type (PRTS 术语释义 屏障 "若无特殊说明,屏障可吸收全种类伤害"). Shields are spent oldest first.
- `visible: true` emits `['status', id, key, 1/0]` client events. `battle.removeBuff(unit, key|buff)`.
**Mod keys** — additive: `atkFlat atkPct defFlat defPct hpFlat hpPct resFlat aspd batPct blockCnt rangeExtend
**Mod keys** — additive: `atkFlat atkPct atkFinal defFlat defPct hpFlat hpPct resFlat aspd batPct blockCnt rangeExtend
defIgnoreFlat defIgnorePct resIgnoreFlat resIgnorePct dodgePhys dodgeArts spRecoveryFlat maxTargets taunt hpRegen
hpRegenRatio spCostFlat moveFlat massFlat blockRadiusScale` (重量 levels: 失重 = `massFlat: −1`; never edit `base.massLevel`;
`blockRadiusScale` = 阻挡半径倍率 − 1, the air-block radius of §1.2);
+2 -2
View File
@@ -2733,8 +2733,8 @@
"algorithm": [
"Order: 阿戈尔 members sorted leftmost first, then topmost (\"更靠左和靠上\").",
"Each 阿戈尔 in order marks the unit on the tile directly in front of it (its facing direction) and also the front-tile unit of every 阿戈尔 it has marked (chain). It never marks itself, a unit it already marked, or a unit that marked it.",
"The marker immediately gains the base ATK of every unit it marked (added to base ATK at the final stage) and their block counts.",
"After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target that dies cancels its remaining pending marks.",
"The marker immediately gains the base ATK of every unit it marked (a 最终加算: added after its ATK percentages, PRTS \"基础攻击力(最终加算)\") and their block counts.",
"After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target that dies cancels its remaining pending marks; a marker knocked out earlier in the pass still resolves its own marks (only the target's state cancels a mark).",
"Each devoured unit adds layers to 阿戈尔 equal to its tier (1-6), once per unit per battle.",
"(5-member tier) Revive is implemented as: when the unit leaves the field for any reason other than being moved, its next deployment has 0 redeploy time and 0 cost (PRTS). Devour layers: each devoured unit adds its tier once per battle (refreshes next round)."
],
+2 -2
View File
@@ -378,8 +378,8 @@ Members (11 chess, 10 in current shop pool; by tier in shop: {'1': 1, '2': 1, '3
- Algorithm:
1. Order: 阿戈尔 members sorted leftmost first, then topmost ("更靠左和靠上").
2. Each 阿戈尔 in order marks the unit on the tile directly in front of it (its facing direction) and also the front-tile unit of every 阿戈尔 it has marked (chain). It never marks itself, a unit it already marked, or a unit that marked it.
3. The marker immediately gains the base ATK of every unit it marked (added to base ATK at the final stage) and their block counts.
4. After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target knocked out for the first time has its pending marks cancelled ("目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果") — also when it is back at once (the 5-tier revive, 不屈's 立刻重新部署, 埃芒加德, M3茧甲), so a revived member is not devoured again. DESIGN §22.3.
3. The marker immediately gains the base ATK of every unit it marked — a 最终加算, added after its ATK percentages (PRTS "该付与来源获得所有标记单位的基础攻击力(最终加算)和阻挡数"; until 0.1.3 the engine added it before them, so a skill's ATK +% scaled it — DESIGN §24.7) — and their block counts.
4. After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target knocked out for the first time has its pending marks cancelled ("目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果") — also when it is back at once (the 5-tier revive, 不屈's 立刻重新部署, 埃芒加德, M3茧甲), so a revived member is not devoured again. DESIGN §22.3. Only the target's state cancels a mark: a marker knocked out earlier in the pass still resolves its own marks, credited to it (DESIGN §24.7; until 0.1.3 they were dropped).
5. Each devoured unit adds layers to 阿戈尔 equal to its tier (1-6), once per unit per battle.
6. (5-member tier) Revive is implemented as: when the unit leaves the field for any reason other than being moved, its next deployment has 0 redeploy time and 0 cost (PRTS). Devour layers: each devoured unit adds its tier once per battle (refreshes next round).
- Formulas: `hpMultiplier = 1.35 + 0.01*L`; `devourDamage = 5000`; `layersPerDevoured = tier of devoured unit`
+5 -3
View File
@@ -130,7 +130,7 @@ export class BattleStatus {
}
/**
* Apply a catalogue status. opts: { duration, source, value, force, refresh, point, resistApplied, stackAs } — returns
* Apply a catalogue status. opts: { duration, source, value, force, refresh, point, resistApplied, stackAs, reenter } — returns
* true when applied. Honours enemy immunities (stun/silence/sleep/frozen/levitate/feared) unless `force`. `beforeStatus`
* handlers may cancel it or change `duration` / `value`. Official rules (buffs.js STATUS): 抵抗 (the `resist` status)
* shortens the RESIST_STATUSES by its value (default half; applied after `beforeStatus`; `resistApplied` skips that
@@ -144,7 +144,9 @@ export class BattleStatus {
* ([r, c] or {x, y}; default the source's tile — a new application moves the point); 恐惧 (`fear`) stamps where it
* was applied and from where (fear.js stampFear: the fan of 恐惧可达地块 its movement uses). A stunned/sleeping operator
* releases the enemies it blocks; a feared/levitated/unblockable/attracted enemy is released by its blocker.
* `statusApplied` reports the final duration and `entered` (the target did not carry the status before).
* `statusApplied` reports the final duration and `entered` (the target did not carry the status before) — or, with
* `reenter`, entered anyway: a pulse whose own short status the caller re-applies as a fresh one each time (缇缇 S2's
* sleep ward, DESIGN §24.8); the buff itself is refreshed as usual.
* A unit that is 无敌 and 无法选中 at once (a 重生 in progress, a hovering or 永久无敌 leader part) takes no status from
* the other side, `force` included — PRTS 无敌 "无法被不同阵营选中": so a status carried by the very hit that knocked an
* enemy out does not land after its 重生's cleanse (DESIGN §21.4). A ground enemy's status never lands on an airborne
@@ -198,7 +200,7 @@ export class BattleStatus {
}
const source = opts.source ?? null;
let entered = true;
for (const b of target.buffs) if ((b.status ?? b.key) === key) { entered = false; break; }
if (opts.reenter !== true) for (const b of target.buffs) if ((b.status ?? b.key) === key) { entered = false; break; }
if (tpl.palsy) {
this.addBuff(target, { ...palsyBuff(value ?? 1), duration, source });
} else if (tpl.valued != null && typeof tpl.mods === 'function' && opts.refresh == null) {
+2 -2
View File
@@ -16,9 +16,9 @@ function resistPalsyDecay({ battle, unit }) {
if (--p.stacks <= 0) battle.removeBuff(unit, p); else unit.markDirty();
}
/** Additive mod keys (summed; × stacks). */
/** Additive mod keys (summed; × stacks). `atkFinal` = 最终加算: added after the percentages (units.js _recalc). */
export const ADD_KEYS = Object.freeze([
'atkFlat', 'atkPct', 'defFlat', 'defPct', 'hpFlat', 'hpPct', 'resFlat', 'aspd', 'batPct', 'blockCnt',
'atkFlat', 'atkPct', 'atkFinal', 'defFlat', 'defPct', 'hpFlat', 'hpPct', 'resFlat', 'aspd', 'batPct', 'blockCnt',
'rangeExtend', 'defIgnoreFlat', 'defIgnorePct', 'resIgnoreFlat', 'resIgnorePct', 'dodgePhys', 'dodgeArts',
'spRecoveryFlat', 'maxTargets', 'taunt', 'hpRegen', 'hpRegenRatio', 'spCostFlat', 'moveFlat', 'massFlat',
// 阻挡半径倍率 (PRTS 数值范围 BLOCK_RADIUS_SCALE, default 1): the air-block radius 0.8944 × (1 + Σ) — Battle._checkBlock
+16 -8
View File
@@ -15,7 +15,8 @@
// ex_damage_scale_per_stack·L) instead; 6: cold wind (devices.js kjeragColdWind)
// 拉特兰 lateranoShip member ammo skills start with floor(ammo × (1 + base_ammo_percent + ammo_percent_per_stack·L));
// 6: every ammo used by a member → all members ATK +atk_per_consume (≤ max_atk_for_consume)
// 阿戈尔 egirShip members max HP +(base_max_hp + max_hp_per_stack·L) (直接乘算); battle start devour (see devour());
// 阿戈尔 egirShip members max HP +(base_max_hp + max_hp_per_stack·L) (直接乘算); battle start devour (see devour():
// the marker's gained base ATK is a 最终加算, `atkFinal`, not scaled by its ATK +%);
// 5: the first max_free_respawn_cnt members knocked out (in knock-out order, the devour's food
// included) each redeploy at once (free) on that first knock-out
// 叙拉古 siracusaShip every member deployment: ASPD +(base + per·L) for (base_duration + per·L) s; 6: 隐匿 for the same
@@ -354,14 +355,20 @@ function egirDownAtStart(battle, u) {
* the operator on the tile in front of them (one step along each member's own direction `dir`), and through marked
* members the tiles in front of
* those (chain); never themselves, a unit already marked by them or a unit that marked them. The marker gains the base
* ATK (atkFlat) and block count of everything it marked; then each mark makes its target lose damage_value HP as a
* ATK and block count of everything it marked — the ATK as a 最终加算 (`atkFinal`, PRTS 盟约记录 "该付与来源获得所有标记单位
* 的基础攻击力(最终加算)和阻挡数"): added after its percentages, so its skill's ATK +% does not scale it (it was `atkFlat`
* until 0.1.3: 1000 base ATK, +100 %, +2000 devoured gave 6000 instead of 4000; GitHub #165 point 2, PR #176, the owner's
* decision of 2026-10-06, DESIGN §24.7). Then each mark makes its target lose damage_value HP as a
* 物理流失 (PRTS 盟约记录: "造成5000点物理流失", 修正 "【吞噬】的物理流失来源为被付与目标自身;单位被【吞噬】击杀时,击杀来源始终为
* 对应标记的付与来源"; PRTS 作战机制: a 物理流失 "会受到目标当前防御力…影响而相应衰减") — less the target's DEF as a physical hit
* (its own source: no DEF ignore), then battle.loseHp: no shields, dodge or damage multipliers (DEF-free until 0.1.1); the
* kill is credited to the marker — in marking order. A target knocked out during the pass has its remaining marks
* cancelled, also when it is back at once (the 5-tier 立刻复活, 不屈's 立刻重新部署, 埃芒加德 / M3茧甲): PRTS 盟约记录 "目标首次被
* 击倒后解除自身被付与但还未触发的【吞噬】效果". A marker off the field gives no further mark (the rule since 0.1.0; one knocked
* out and back in the same pass still gives its marks).
* 击倒后解除自身被付与但还未触发的【吞噬】效果". Only the target's state cancels a mark: a marker knocked out by an earlier
* mark still resolves its own, credited to it (PRTS names only the target; "击杀来源始终为对应标记的付与来源") — GitHub #165
* point 3, PR #176, the owner's decision of 2026-10-06; until 0.1.3 a marker off the field gave no further mark.
* Not modelled: the 流失's own source (PRTS: the target itself) — the hooks see the marker as the source, as for the
* credit (an open question, DESIGN §24.7).
* 联防: the operators down since the end of their own combat (forced out by Battle.start) mark, are marked and resolve
* their marks like standing ones, but nothing resolves on them (below; per players' reports, owner's decision 2026-10-04).
* Whose operator stands in front does not matter (PRTS "依次吞噬身前一格干员", no own-side limit; the owner's decision of
@@ -410,7 +417,7 @@ function devour(battle, pid, bb, members) {
let atk = 0, block = 0;
for (const t of mine) { atk += num(t.base.atk, 0); block += num(t.base.blockCnt, 0); marks.push([m, t]); }
const mods = {};
if (atk > 0) mods.atkFlat = atk;
if (atk > 0) mods.atkFinal = atk; // 最终加算 (units.js _recalc)
if (block > 0) mods.blockCnt = block;
if (Object.keys(mods).length) S.passiveBuff(battle, m, 'bond:egir:devour', mods);
}
@@ -423,9 +430,10 @@ function devour(battle, pid, bb, members) {
for (const [, t] of marks) if (!dep.has(t)) dep.set(t, items.deploymentOf(t));
const knocked = (t) => !t.alive || items.deploymentOf(t) !== dep.get(t);
for (const [m, t] of marks) {
// (a member down since its own combat — downAtStart — resolves its marks as if it stood; a mark on it resolves nothing:
// `knocked` — it is off the field)
if (knocked(t) || !(m.alive || downAtStart(m))) continue;
// every mark resolves whatever became of its marker — knocked out by an earlier mark (and revived or not), or down
// since its own combat (downAtStart); a mark on a unit knocked out during the pass, or lying down since the 联防
// start, resolves nothing (`knocked`: it is off the field)
if (knocked(t)) continue;
S.fxOn(battle, 'devour', t, 'bond:egirShip', 'devour', { from: m.id });
if (amount > 0) battle.loseHp(t, mitigate(amount, 'phys', t.s), { source: m, tags: ['bond:egir:devour'] });
if (!layered.has(t)) {
+3 -1
View File
@@ -14,7 +14,9 @@
// 'freeze', × prob) · act2autochess_gar_event_onstart (every deploy) ·
// act2autochess_gar_event_allyenemy_sleepstun_inrange (an enemy or operator in range ENTERS
// sleep / stun). "进入…时": re-applying a running status (a refresh) is not a new entry
// (engine statusApplied ctx.entered). Targets: bond_by_id / bond_self (own active bonds) / bond_actived_maxstack;
// (engine statusApplied ctx.entered) — except a pulse that re-applies its own short status as a
// fresh one (applyStatus `reenter`: 缇缇 S2's sleep ward, DESIGN §24.8).
// Targets: bond_by_id / bond_self (own active bonds) / bond_actived_maxstack;
// amounts: by_count / by_charcount_samerow / by_charlevel; conditions character_same_row /
// character_same_col (≥ check_count incl. self). Gains go through support.gainLayers with
// reason 'garrison', source = the trait's owner, cap = max_add_count_per_battle per (instance, bond).
@@ -20,7 +20,8 @@ export default {
// enemies, sleeping enemies take 30 % ATK arts/s. T2 勇气的报偿: Sargon/Minos ops above 50 % HP get +20 ASPD.
// S1 缓蚀 (duration): ATK +, attack@prob chance per attack to sleep the target attack@sleep s.
// S2 封护 (duration): no attacks, ATK +; she and the lowest-HP-ratio operator in her range sleep until the skill ends
// (沉睡 = invulnerable), enemies around either of them keep falling asleep; T1 ×talent_scale.
// (沉睡 = invulnerable), enemies around either of them keep falling asleep — each 0.25 s pulse a new 沉睡 entry for
// her trait (GitHub #162, DESIGN §24.8); T1 ×talent_scale.
chess_char_5_02_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1);
const sid = selectedId(chess, def);
@@ -61,9 +62,22 @@ export default {
unit.mem.titiWardAcc += dt;
if (unit.mem.titiWardAcc < AURA_IV - 1e-9) return;
unit.mem.titiWardAcc = 0;
// "期间持续使自身与该目标周围的敌人陷入沉睡": every AURA_IV (0.25 s) the enemies around a ward sleep AURA_DUR (0.5 s),
// and each pulse is a new 沉睡 entry (`reenter`), so her trait garrison_125 「每当范围内有敌人或干员进入沉睡或晕眩时」
// climbs on it to its per-battle cap (GitHub #162, the reporter's memory of the official mode: 「攻击范围内有陷入
// 沉睡就开始迅速增加直至上限」; the owner's decision of 2026-10-06, DESIGN §24.8). [ASSUMED] the pulse timing (not in
// the data). Once per enemy and pulse (one beside both wards re-enters once); not while a longer sleep from
// elsewhere holds it. The 0.5 s sleep outlasts the 0.25 s, so the enemy never wakes between pulses — only the
// `entered` flag of the pulse changed (until 0.1.3 the first pulse alone was an entry).
const pulsed = new Set();
for (const a of unit.mem.titiWard || []) {
if (!a.alive || !a.deployed || !a.findBuff('titi:ward')) continue;
for (const e of battle.foesInRadius(a.x, a.y, RING1)) if (e.alive) battle.applyStatus(e, 'sleep', { duration: AURA_DUR, source: unit });
for (const e of battle.foesInRadius(a.x, a.y, RING1)) {
if (!e.alive) continue;
const reenter = !pulsed.has(e) && !e.buffs.some((b) => (b.status ?? b.key) === 'sleep' && b.timeLeft > AURA_DUR + 1e-9);
pulsed.add(e);
battle.applyStatus(e, 'sleep', { duration: AURA_DUR, source: unit, reenter });
}
}
},
onEnd({ battle, unit }) {
+3 -1
View File
@@ -118,7 +118,9 @@ export class Unit {
const b = this.base;
const bHp = fin(b.maxHp, 1) > 0 ? fin(b.maxHp, 1) : 1;
const maxHp = Math.max(1, fin((bHp + a('hpFlat')) * Math.max(0, 1 + a('hpPct')) * m('hpMul'), bHp));
const atk = Math.max(0, fin((b.atk + a('atkFlat')) * Math.max(0, 1 + a('atkPct')) * m('atkMul'), fin(b.atk, 0)));
// PRTS 游戏数据基础 属性基本公式 A_f = F_t[(A + D_p)(1 + D_t) + F_p]: `atkFinal` is the 最终加算 (FINAL_ADDITION) —
// added after the percentages, inside the Πmul (阿戈尔's devoured base ATK, DESIGN §24.7)
const atk = Math.max(0, fin(((b.atk + a('atkFlat')) * Math.max(0, 1 + a('atkPct')) + a('atkFinal')) * m('atkMul'), fin(b.atk, 0)));
const def = Math.max(0, fin((b.def + a('defFlat')) * Math.max(0, 1 + a('defPct')) * m('defMul'), fin(b.def, 0)));
const res = clamp(fin((b.res + a('resFlat')) * m('resMul'), fin(b.res, 0)), 0, 100);
const aspd = clamp(fin(b.aspd + a('aspd'), 100), ASPD_MIN, ASPD_MAX);
+48 -4
View File
@@ -335,7 +335,29 @@ test('阿戈尔: HP ×(1.35+0.01L); devour chain (left first): 5000 物理流失
checkInvariants(h.b);
});
test('阿戈尔 devour: 物理流失 ignores the marker’s damage bonuses and the target’s shields; a dead marker’s marks are cancelled; kill → marker', () => {
test('阿戈尔 devour ATK is a 最终加算 (GitHub #165 point 2, PR #176): 1000 base ATK, +100 % skill ATK, +2000 devoured → 4000, not 6000', () => {
// PRTS 盟约记录 "该付与来源获得所有标记单位的基础攻击力(最终加算)"; PRTS 游戏数据基础 A_f = F_t[(A + D_p)(1 + D_t) + F_p]
const list = [
['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']],
['fod_a', ['preciShip'], { stats: { atk: 2000, maxHp: 20000, def: 0, blockCnt: 1 } }],
];
// g1 (10,3) → fodder (10,4, 2000 base ATK); g2 / g3 face empty tiles
const units = [{ chessId: 'g1_a', row: 10, col: 3 }, { chessId: 'fod_a', row: 10, col: 4 }, { chessId: 'g2_a', row: 12, col: 3 }, { chessId: 'g3_a', row: 12, col: 5 }];
const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(3, 0, null, [3, 5]) } });
h.step(1);
const g1 = h.unit('g1_a');
assert.deepEqual(buffOf(g1, 'bond:egir:devour')?.mods, { atkFinal: 2000, blockCnt: 1 }, 'the fodder\'s base ATK as a 最终加算, its block count');
close(g1.s.atk, 1000 + 2000, 1e-6, 'no percentage: as before');
h.b.addBuff(g1, { key: 'test:skill', mods: { atkPct: 1 } });
close(g1.s.atk, 1000 * 2 + 2000, 1e-6, '+100 %: 4000 (0.1.3: (1000 + 2000) × 2 = 6000)');
h.b.addBuff(g1, { key: 'test:flat', mods: { atkFlat: 100 } });
close(g1.s.atk, (1000 + 100) * 2 + 2000, 1e-6, 'a 直接加算 is still scaled by the percentages');
h.b.addBuff(g1, { key: 'test:weaken', mods: { atkMul: 0.5 } });
close(g1.s.atk, ((1000 + 100) * 2 + 2000) * 0.5, 1e-6, 'the 最终乘算 (Πmul) scales the whole, the 最终加算 included');
checkInvariants(h.b);
});
test('阿戈尔 devour: 物理流失 ignores the marker’s damage bonuses and the target’s shields; a fallen marker’s marks still resolve (GitHub #165 point 3); kill → marker', () => {
const list = [
['g1_a', ['egirShip', 'kjeragShip']], ['g2_a', ['egirShip'], { stats: { maxHp: 3000 } }], ['g3_a', ['egirShip']],
['fod_a', ['preciShip'], { tier: 2, stats: { maxHp: 20000, def: 500 } }],
@@ -353,14 +375,36 @@ test('阿戈尔 devour: 物理流失 ignores the marker’s damage bonuses and t
assert.ok(!g2.alive, 'g2 devoured');
assert.ok(h.hooksOf('kill').some((c) => c.victim === g2 && c.killer === g1), 'kill credited to the marker');
const dv = tagged(h, 'bond:egir:devour');
assert.deepEqual(dv.map((c) => [c.source.defId, c.target.defId]), [['g1_a', 'g2_a'], ['g1_a', 'fod_a']], 'g2’s own mark on the fodder is cancelled');
close(f.hp, 20000 - (5000 - 500), 1e-6, '5000 less its DEF 500: no ×1.25, shield untouched');
// PRTS 盟约记录: only the knocked-out TARGET's pending marks are cancelled — g2, knocked out by the first mark, still
// resolves its own (until 0.1.3 a marker off the field gave no further mark)
assert.deepEqual(dv.map((c) => [c.source.defId, c.target.defId]), [['g1_a', 'g2_a'], ['g1_a', 'fod_a'], ['g2_a', 'fod_a']], 'g2’s own mark on the fodder resolves');
close(f.hp, 20000 - 2 * (5000 - 500), 1e-6, '5000 less its DEF 500, twice: no ×1.25, shield untouched');
assert.equal(f.buffs.find((b) => b.key === 'test:shield')?.shield, 3000);
close(g1.s.atk, 1000 + 1000 + 1000, 1e-6, 'the base ATK of everything it marked stays');
assert.equal(h.b.getPlayer('p1').bonds.egirShip.layers, 1 + 2, 'layers = tiers of the devoured units');
checkInvariants(h.b);
});
test('阿戈尔 devour, A → B → C with B devoured first (GitHub #165 point 3, PR #176): C still takes B\'s 5000 and the kill is B\'s', () => {
const list = [
['A_a', ['egirShip']], ['B_a', ['egirShip'], { stats: { maxHp: 3000 } }], ['g3_a', ['egirShip']],
['C_a', ['preciShip'], { tier: 2, stats: { maxHp: 9000, def: 0 } }],
];
// A (10,3) → B (10,4, 3000 × 1.35 HP: knocked out by A's mark) → C (10,5, 9000 HP, DEF 0: A's mark leaves 4000, B's
// knocks it out); g3 faces an empty tile. Marks: A → B, A → C (through B), B → C.
const units = [{ chessId: 'A_a', row: 10, col: 3 }, { chessId: 'B_a', row: 10, col: 4 }, { chessId: 'C_a', row: 10, col: 5 }, { chessId: 'g3_a', row: 12, col: 3 }];
const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(3, 0, null, [3, 5]) }, hooks: ['damaged', 'kill'], captureNoisy: true });
h.step(1);
const [A, B, C] = ['A_a', 'B_a', 'C_a'].map((id) => h.unit(id));
assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]), [['A_a', 'B_a'], ['A_a', 'C_a'], ['B_a', 'C_a']]);
assert.ok(!B.alive && !C.alive, 'B falls to A\'s mark, C to B\'s (until 0.1.3 B\'s mark was dropped: C stood at 4000 HP)');
// PRTS 盟约记录 "单位被【吞噬】击杀时,击杀来源始终为对应标记的付与来源"
assert.deepEqual(h.hooksOf('kill').map((c) => [c.victim.defId, c.killer?.defId]), [['B_a', 'A_a'], ['C_a', 'B_a']], 'the kill credit is the mark\'s marker, down or not');
close(A.s.atk, 1000 + 1000 + 1000, 1e-6, 'A: the base ATK of B and C');
assert.equal(h.b.getPlayer('p1').bonds.egirShip.layers, 1 + 2, 'each devoured unit adds its tier once');
checkInvariants(h.b);
});
test('阿戈尔 devour order: left first, then top first (row 0 is the bottom: the top board row is 12)', () => {
const list = [['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']], ['fod_a', ['preciShip']]];
const units = [
@@ -600,7 +644,7 @@ test('阿戈尔 devour, a unit back during the pass: a marker revived by the 5-t
h.step(1);
const [g2, f] = ['g2_a', 'fod_a'].map((id) => h.unit(id));
assert.ok(g2.alive && h.hooksOf('deploy').some((c) => c.unit === g2 && !c.initial), 'g2 knocked out and revived');
// only a marker off the field gives no further mark (the rule since 0.1.0): g2, back at once, still devours g3 and the fodder
// every mark resolves whatever became of its marker (DESIGN §24.7): g2, back at once, still devours g3 and the fodder
assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]),
[['g1_a', 'g2_a'], ['g1_a', 'g3_a'], ['g1_a', 'fod_a'], ['g2_a', 'g3_a'], ['g2_a', 'fod_a'], ['g3_a', 'fod_a']]);
close(f.hp, 20000 - 3 * 5000, 1e-6, 'the fodder: g1, g2 and g3');
+3 -3
View File
@@ -39,7 +39,7 @@ test('联防 阿戈尔 chain 乌尔比安 → 幽灵鲨 (down) → 歌蕾蒂娅
const got = buffOf(ulpia, 'bond:egir:devour');
assert.ok(ref && got, 'both 乌尔比安 devour');
const base = (id) => down.unit(id).base;
close(got.mods.atkFlat, base(GHOST).atk + base(GLADY).atk + base(FODDER).atk, 1e-6, 'the base ATK of 幽灵鲨, 歌蕾蒂娅 and the fodder');
close(got.mods.atkFinal, base(GHOST).atk + base(GLADY).atk + base(FODDER).atk, 1e-6, 'the base ATK of 幽灵鲨, 歌蕾蒂娅 and the fodder');
assert.equal(got.mods.blockCnt, base(GHOST).blockCnt + base(GLADY).blockCnt + base(FODDER).blockCnt);
assert.deepEqual(got.mods, ref.mods, 'exactly the gains of the chain with 幽灵鲨 standing');
close(ulpia.s.atk, up.unit(ULPIA).s.atk, 1e-6, '乌尔比安 ATK');
@@ -57,7 +57,7 @@ test('联防 阿戈尔 chain 乌尔比安 → 幽灵鲨 (down) → 歌蕾蒂娅
assert.equal(fodder.alive, up.unit(FODDER).alive);
// her own marks (歌蕾蒂娅, the fodder) still give her their base ATK / block for when she is back
const own = buffOf(ghost, 'bond:egir:devour');
close(own ? own.mods.atkFlat : 0, base(GLADY).atk + base(FODDER).atk, 1e-6, '幽灵鲨 keeps her marking gains (a persistent buff)');
close(own ? own.mods.atkFinal : 0, base(GLADY).atk + base(FODDER).atk, 1e-6, '幽灵鲨 keeps her marking gains (a persistent buff)');
checkInvariants(down.b);
assert.equal(down.result().perPlayer.p1.deaths, up.result().perPlayer.p1.deaths - 1, 'no knock-out counted for her (standing, the devour knocks her out)');
});
@@ -84,7 +84,7 @@ test('联防 阿戈尔: a down member at the head of the chain devours as if it
const down = head(true, five);
const [ghost, glady, fodder] = [GHOST, GLADY, FODDER].map((id) => down.unit(id));
const g = buffOf(ghost, 'bond:egir:devour');
close(g.mods.atkFlat, glady.base.atk + fodder.base.atk, 1e-6, `${tag}: marks 歌蕾蒂娅 and, through her, the fodder`);
close(g.mods.atkFinal, glady.base.atk + fodder.base.atk, 1e-6, `${tag}: marks 歌蕾蒂娅 and, through her, the fodder`);
const pairs = (h) => devours(h).map((c) => [c.source.defId, c.target.defId]);
assert.deepEqual(pairs(down), pairs(up), `${tag}: the marks resolve as with 幽灵鲨 standing`);
assert.deepEqual(pairs(down), [[GHOST, GLADY], [GHOST, FODDER]], `${tag}: her two marks; 歌蕾蒂娅's on the fodder is cancelled (knocked out first)`);
+3 -3
View File
@@ -52,7 +52,7 @@ test('联防: an 阿戈尔 devours a teammate\'s living operator in front — th
const ref = buffOf(solo.unit(1), 'bond:egir:devour');
const fodderBase = solo.unit(100).base;
assert.ok(ref, 'solo: 乌尔比安 devours the operator in front');
close(ref.mods.atkFlat, fodderBase.atk, 1e-9, 'its base ATK');
close(ref.mods.atkFinal, fodderBase.atk, 1e-9, 'its base ATK');
assert.equal(ref.mods.blockCnt, fodderBase.blockCnt, 'its block count');
for (const [tag, h] of [['own', own], ['teammate\'s', mate]]) {
const ulpia = h.unit(1), t = h.unit(100);
@@ -92,7 +92,7 @@ test('联防: the chain goes on through a teammate\'s 阿戈尔 — the gains eq
const p2Units = [HN, MIZUKI, GLADY, DEEP].map((chessId, i) => ({ uid: 200 + i, kind: 'chess', chessId, row: 12, col: 3 + 2 * i, dir: 'RIGHT' }));
const mate = field({ owner: 'mate', front: [GHOST, FODDER], p2Units, p2Bonds: { egirShip: bondOn(5) } });
const ref = buffOf(own.unit(1), 'bond:egir:devour');
close(ref.mods.atkFlat, own.unit(100).base.atk + own.unit(101).base.atk, 1e-9, 'own chain: 幽灵鲨 and, through her, the fodder');
close(ref.mods.atkFinal, own.unit(100).base.atk + own.unit(101).base.atk, 1e-9, 'own chain: 幽灵鲨 and, through her, the fodder');
assert.deepEqual(buffOf(mate.unit(1), 'bond:egir:devour')?.mods, ref.mods, 'the teammate\'s chain gives the same');
assert.deepEqual(devours(mate).map((c) => [c.source.uid, c.target.uid]), devours(own).map((c) => [c.source.uid, c.target.uid]));
const ghost = mate.unit(100);
@@ -134,7 +134,7 @@ test('联防 through unite.js: food knocked out by the own combat\'s devour ente
u.step();
const ulpia = u.unit(1);
assert.deepEqual(buffOf(ulpia, 'bond:egir:devour')?.mods, gain, 'the own combat\'s gains');
close(gain.atkFlat, [2, 3, 4].reduce((s, uid) => s + u.unit(uid).base.atk, 0), 1e-9, '幽灵鲨 + 歌蕾蒂娅 + fodder base ATK');
close(gain.atkFinal, [2, 3, 4].reduce((s, uid) => s + u.unit(uid).base.atk, 0), 1e-9, '幽灵鲨 + 歌蕾蒂娅 + fodder base ATK');
assert.equal(gain.blockCnt, [2, 3, 4].reduce((s, uid) => s + u.unit(uid).base.blockCnt, 0));
for (const uid of [2, 3, 4]) assert.ok(!u.unit(uid).alive && u.unit(uid).removeReason === FORCED_EXIT, `${uid} forced out`);
assert.equal(u.b.getPlayer('p1').bonds.egirShip.layers, 10, 'no layers in 联防');
+53
View File
@@ -302,6 +302,59 @@ test('allyenemy_sleepstun_inrange (缇缇 125): enemies or operators entering
assert.equal(capOf('garrison_125_b'), 48);
});
test('缇缇 125 + S2 封护 (GitHub #162): every 0.25 s sleep pulse of the ward is a new entry — 8 s climb to the cap; a sleeper outside her range does not count', () => {
// the reporter's memory of the official mode: 「攻击范围内有陷入沉睡就开始迅速增加直至上限」; the pulse timing is [ASSUMED]
for (const id of ['chess_char_5_02_a', 'chess_char_5_02_b']) {
const gid = D.chess[id].garrisonIds.find((g) => GR(g).effectKey === 'act2autochess_gar_event_allyenemy_sleepstun_inrange');
const per = num(GR(gid).bb.bond_add_count), cap = capOf(gid);
const s2 = D.chess[id].skills.find((x) => x.skillId === 'skchr_titi_2').index;
// 缇缇 (10,4) facing right: range 3-3 = rows 9–11, cols 4–7. The ward ally (10,6) is the only operator in it; the
// enemy (10,5) stands beside both wards (in her range), the one at (10,3) beside her back (out of her range)
const run = (enemies) => {
const h = makeBattle({
seed: 3, autoFinish: false, timeLimit: 400, hooks: ['statusApplied'],
defs: { chess: { ward: rec('ward', []) }, enemies: { e_dummy: dummy() } },
units: [{ chessId: id, skillIndex: s2, row: 10, col: 4 }, { chessId: 'ward', row: 10, col: 6 }],
bonds: { sargonShip: B(0), preciShip: B(0) }, enemies,
});
h.step(1);
const tt = h.unit(id);
tt.skill.gainSp(1000);
assert.ok(tt.skill.activate('test'), `${id}: S2 cast`);
assert.equal(tt.skill.id, 'skchr_titi_2');
return { h, tt };
};
// out of range only: the pulses sleep it (each one an entry for the engine) but her trait ignores it
{
const { h, tt } = run([{ key: 'e_dummy', pos: [10, 3] }]);
const back = h.enemies()[0];
expectAll(h, ['sargonShip', 'preciShip'], 2 * per, `${id}: the ward's two operators fall asleep in her range`);
h.run(8);
assert.ok(tt.skill.active && back.s.flags.sleep, `${id}: the enemy at her back sleeps`);
const pulses = h.hooksOf('statusApplied').filter((c) => c.target === back && c.source === tt && c.entered);
assert.ok(pulses.length >= 30, `${id}: ${pulses.length} pulse entries in 8 s`);
expectAll(h, ['sargonShip', 'preciShip'], 2 * per, `${id}: an out-of-range sleeper does not count`);
checkInvariants(h.b);
}
// in range: one entry per pulse (beside both wards: still one), the cap within the 8 s
{
const { h, tt } = run([{ key: 'e_dummy', pos: [10, 5] }, { key: 'e_dummy', pos: [10, 3] }]);
const near = h.enemies().find((e) => Math.round(e.x) === 5);
let capAt = null;
for (let t = 0; t < 8 - 1e-9; t += 0.25) {
h.run(0.25);
if (capAt == null && (gains(h).sargonShip ?? 0) >= cap) capAt = t + 0.25;
}
assert.ok(tt.skill.active && near.s.flags.sleep, `${id}: still asleep after 8 s`);
const mine = h.hooksOf('statusApplied').filter((c) => c.target === near && c.source === tt && c.status === 'sleep');
assert.equal(mine.filter((c) => c.entered).length * 2, mine.length, `${id}: two wards, one entry per pulse`);
expectAll(h, ['sargonShip', 'preciShip'], cap, `${id}: the cap (${cap})`);
assert.ok(capAt != null && capAt <= 6.5, `${id}: capped after ${capAt} s`);
checkInvariants(h.b);
}
}
});
// ---------------------------------------------------------------------------------------------------------------------
// ADD_BOND grants
+12 -12
View File
@@ -220,8 +220,8 @@
"round": 10,
"seed": 3867395638,
"waveId": "act1autochess_h03",
"time": 74.03,
"ticks": 2221,
"time": 89.03,
"ticks": 2671,
"reason": "cleared",
"killed": 19,
"total": 19,
@@ -230,23 +230,23 @@
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":19,"total":19,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":379469,"bossDmg":0,"heal":4635,"deaths":1,"layerGains":{"raidShip":10}}
"p1": {"killed":19,"total":19,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":379469,"bossDmg":0,"heal":4635,"deaths":1,"layerGains":{"raidShip":8}}
},
"units": [
["p1",1,"op","chess_char_1_04_b",1,"uniequip_002_udflow",32931.36,1,2,0,0,0,0,0,0],
["p1",2,"op","chess_char_2_07_b",1,"uniequip_002_ghost",27707.68,1,3,0,0,4635,4635,0,0],
["p1",3,"op","chess_char_3_05_b",2,"uniequip_002_skadi",18673.58,1,11,11,135708,36837,0,45,1],
["p1",3,"op","chess_char_3_05_b",2,"uniequip_002_skadi",19561.92,1,11,9,153631,35961,0,57,1],
["p1",4,"op","chess_char_4_16_b",2,"uniequip_002_texas2",1619,1,17,0,0,1619,0,0,2],
["p1",5,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",20727.84,1,6,4,186331,0,0,12,2],
["p1",6,"op","chess_char_3_09_b",1,"uniequip_002_haini",17395.56,1,7,0,23243,0,0,20,1],
["p1",7,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,8,4,34187,0,0,35,15],
["p1",5,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",20727.84,1,6,5,133052,0,0,14,2],
["p1",6,"op","chess_char_3_09_b",1,"uniequip_002_haini",17395.56,1,7,1,44900,0,0,54,2],
["p1",7,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,8,4,47885,0,0,54,21],
["p1",8,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",1626,1,1,0,0,0,0,0,0]
],
"enemies": {"enemy_1229_darmy":[10,10,0,4638,104509],"enemy_1229_darmy_2":[7,7,0,18088,95800],"enemy_9007_acelem":[2,2,0,4609,179159]},
"hooks": [1,29,2221,148,148,256,276,251,20,21,21,20,0,4154,485,13,19,0,1,20,5,3,0],
"events": {"atk":167,"deploy":10,"die":21,"dmg":276,"engage":4,"fx":34,"layer":5,"skill":41,"spawn":27,"status":132},
"fx": {"beam":2,"blink":1,"burst":3,"devour":4,"dilemma":2,"frostNova":15,"garrison":5,"swordStorm":2},
"snaps": "066fec91 aec41fca b75071e0 478fe370 a68e442a 1b0b0907 cc527129 08c4150f"
"enemies": {"enemy_1229_darmy":[10,10,0,6146,104509],"enemy_1229_darmy_2":[7,7,0,9732,95800],"enemy_9007_acelem":[2,2,0,10581,179159]},
"hooks": [1,29,2671,206,206,337,354,251,20,21,28,28,0,5849,650,12,19,0,1,20,4,2,0],
"events": {"atk":222,"deploy":10,"die":21,"dmg":354,"engage":4,"fx":38,"layer":4,"skill":56,"spawn":27,"status":180},
"fx": {"beam":2,"blink":1,"burst":2,"devour":4,"dilemma":2,"frostNova":21,"garrison":4,"swordStorm":2},
"snaps": "bc558eb6 7676040f 1d3db929 9a6a9f81 69a155b8 633af258 443f1b65 60398dc5 f22ce2d0"
},
"bond-emptyShip-low": {
"about": "协防干员 low: tier 1 layers 1; chess_char_4_01_a chess_char_4_04_a chess_char_2_14_a chess_char_3_10_a chess_char_3_20_a chess_char_3_21_a chess_char_4_02_a chess_char_4_06_a",
+14 -14
View File
@@ -1382,7 +1382,7 @@
"combat": "client",
"boost": {"lp":400,"layers":300},
"setup": {"stageId":"act2autochess_m04","bossId":"boss_5","hiddenBossId":"boss_9","factions":["FLY","ELEMENT","INVISIBLE"]},
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":800,"bossPoolLeft":0,"virtualMs":126150},
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":793,"bossPoolLeft":0,"virtualMs":124650},
"rng": {"setup":25,"shop":674,"waves":0,"draft":61,"bots":22077,"meta":140},
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
"rounds": [
@@ -1423,33 +1423,33 @@
},
{
"round": 8,
"ai_0": [1,400,12,0,0,4,8,"d692537c",1552,"arcaneShip 1/312, deputShip 0/304, egirShip 1/304, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/310, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 2/322, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,18,0,64],
"ai_0": [1,400,12,0,0,4,8,"d692537c",1558,"arcaneShip 1/315, deputShip 0/304, egirShip 1/307, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/310, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 2/322, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,18,0,64],
"ai_1": [1,400,11,0,3,4,8,"a8a41789",1261,"arcaneShip 0/302, deputShip 2/314, egirShip 0/300, emptyShip 0/300, indomShip 1/304, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/314, steadShip 1/329, suntShip 0/300, swiftShip 0/300, victoriaShip 0/305, visiShip 0/300, yanShip 0/305",20,20,0,67]
},
{
"round": 9,
"ai_0": [1,400,12,0,0,4,8,"8469d2a6",2226,"arcaneShip 2/315, deputShip 0/304, egirShip 1/323, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/310, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/314, raidShip 1/330, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/330, suntShip 0/300, swiftShip 0/306, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,77],
"ai_0": [1,400,12,0,0,4,8,"8469d2a6",2232,"arcaneShip 2/318, deputShip 0/304, egirShip 1/326, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/310, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/314, raidShip 1/330, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/330, suntShip 0/300, swiftShip 0/306, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,77],
"ai_1": [1,400,15,0,3,4,8,"59b4a75a",1568,"arcaneShip 0/302, deputShip 1/316, egirShip 0/300, emptyShip 0/300, indomShip 1/306, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/302, soloShip 0/314, steadShip 1/336, suntShip 0/300, swiftShip 0/300, victoriaShip 1/308, visiShip 0/300, yanShip 0/305",24,24,0,82]
},
{
"round": 10,
"ai_0": [1,400,12,0,0,5,8,"5e407c98",2262,"arcaneShip 2/320, deputShip 0/304, egirShip 1/334, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/314, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/320, raidShip 1/338, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/332, suntShip 0/300, swiftShip 0/306, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,93],
"ai_0": [1,400,12,0,0,5,8,"5e407c98",2268,"arcaneShip 2/323, deputShip 0/304, egirShip 1/337, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/314, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/320, raidShip 1/338, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/332, suntShip 0/300, swiftShip 0/306, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,93],
"ai_1": [1,400,15,0,0,5,8,"1cb9c2e4",1583,"arcaneShip 0/302, deputShip 1/318, egirShip 0/300, emptyShip 0/300, indomShip 1/308, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/304, soloShip 0/314, steadShip 1/342, suntShip 0/300, swiftShip 0/300, victoriaShip 1/311, visiShip 0/300, yanShip 0/305",12,12,0,97]
},
{
"round": 11,
"ai_0": [1,400,12,0,2,5,8,"cfd3ac7b",2321,"arcaneShip 1/323, deputShip 0/304, egirShip 1/352, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/322, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/324, raidShip 1/360, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/336, suntShip 0/300, swiftShip 0/310, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",34,34,0,105],
"ai_0": [1,400,12,0,2,5,8,"cfd3ac7b",2321,"arcaneShip 1/326, deputShip 0/304, egirShip 1/353, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/322, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/324, raidShip 1/358, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/334, suntShip 0/300, swiftShip 0/310, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",34,34,0,105],
"ai_1": [1,400,12,0,1,6,8,"b6bd931e",2209,"arcaneShip 0/302, deputShip 1/320, egirShip 0/300, emptyShip 0/300, indomShip 1/312, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/306, soloShip 0/314, steadShip 1/349, suntShip 1/302, swiftShip 0/300, victoriaShip 1/316, visiShip 1/304, yanShip 0/305",34,34,0,120]
},
{
"round": 12,
"ai_0": [1,400,14,0,0,6,8,"e7a322b8",2348,"arcaneShip 1/323, deputShip 0/304, egirShip 1/353, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/330, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/332, raidShip 1/370, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/336, suntShip 0/300, swiftShip 0/310, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",6,14,8,119],
"ai_0": [1,400,14,0,0,6,8,"e7a322b8",2348,"arcaneShip 1/326, deputShip 0/304, egirShip 1/354, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/330, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/332, raidShip 1/368, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/334, suntShip 0/300, swiftShip 0/310, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",6,14,8,119],
"ai_1": [1,400,13,0,1,6,8,"58fdf70d",2235,"arcaneShip 0/302, deputShip 1/322, egirShip 0/300, emptyShip 0/300, indomShip 2/314, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/308, soloShip 0/314, steadShip 1/357, suntShip 1/304, swiftShip 0/300, victoriaShip 1/324, visiShip 1/306, yanShip 0/310",15,15,0,134]
},
{
"round": 13,
"ai_0": [1,400,12,0,0,6,8,"feef54ce",2706,"arcaneShip 2/330, deputShip 0/304, egirShip 1/368, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/334, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/340, raidShip 1/390, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/340, suntShip 1/300, swiftShip 0/310, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,133],
"ai_1": [1,400,13,0,2,6,8,"6a6f946a",1962,"arcaneShip 0/302, deputShip 1/324, egirShip 0/300, emptyShip 0/300, indomShip 2/322, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/310, soloShip 0/314, steadShip 1/366, suntShip 1/306, swiftShip 0/300, victoriaShip 1/334, visiShip 0/306, yanShip 0/310",36,41,5,150]
"ai_0": [1,398,12,0,0,6,8,"feef54ce",2700,"arcaneShip 2/333, deputShip 0/304, egirShip 1/367, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/334, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/340, raidShip 1/386, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/336, suntShip 1/300, swiftShip 0/310, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",39,41,2,133],
"ai_1": [1,395,13,0,2,6,8,"6a6f946a",1962,"arcaneShip 0/302, deputShip 1/324, egirShip 0/300, emptyShip 0/300, indomShip 2/322, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/310, soloShip 0/314, steadShip 1/366, suntShip 1/306, swiftShip 0/300, victoriaShip 1/334, visiShip 0/306, yanShip 0/310",36,41,5,150]
}
],
"players": {
@@ -1458,26 +1458,26 @@
"victory": true,
"roundsPassed": 15,
"eliminatedRound": null,
"lp": 400,
"lp": 398,
"bandId": "band_sarkazb",
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
"trophies": 8,
"reward": 425,
"stats": {"activatedLayers":2986,"bossDamage":4110836,"dmgDealt":14169113,"gold":158,"itemsEquipped":18,"kills":295,"leaks":8,"lpLost":0,"merges":7,"perfectRounds":12,"refreshes":25},
"stats": {"activatedLayers":2984,"bossDamage":4099302,"dmgDealt":13617793,"gold":158,"itemsEquipped":18,"kills":291,"leaks":10,"lpLost":2,"merges":7,"perfectRounds":11,"refreshes":25},
"lineup": ["4_09_b@11,5[4_11_e_a+6_09_e_a]","6_11_a@11,8","5_07_a@10,4[1_01_e_b+1_02_e_b]","1_19_b@10,8[5_02_e_a+4_04_e_a]","3_09_b@9,6[3_08_e_a+4_10_e_a]","6_05_a@9,7","4_20_a@9,8[2_01_e_a]","3_05_a@9,9[4_07_e_a+4_06_e_a]"],
"bonds": ["arcaneShip 1/330","deputShip 0/304","egirShip 1/368","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 1/334","kjeragShip 0/302","lateranoShip 0/300","maniShip 1/300","miraShip 0/300","preciShip 1/345","raidShip 1/390","sargonShip 0/300","siracusaShip 0/300","skillfulShip 0/300","soloShip 1/304","steadShip 0/340","suntShip 1/300","swiftShip 1/315","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
"bonds": ["arcaneShip 1/333","deputShip 0/304","egirShip 1/367","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 1/334","kjeragShip 0/302","lateranoShip 0/300","maniShip 1/300","miraShip 0/300","preciShip 1/345","raidShip 1/386","sargonShip 0/300","siracusaShip 0/300","skillfulShip 0/300","soloShip 1/304","steadShip 0/336","suntShip 1/300","swiftShip 1/315","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
},
"ai_1": {
"alive": true,
"victory": true,
"roundsPassed": 15,
"eliminatedRound": null,
"lp": 400,
"lp": 395,
"bandId": "band_doberm",
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
"trophies": 8,
"reward": 425,
"stats": {"activatedLayers":2322,"bossDamage":2829244,"dmgDealt":16856376,"gold":174,"itemsEquipped":28,"kills":280,"leaks":6,"lpLost":0,"merges":4,"perfectRounds":11,"refreshes":40},
"stats": {"activatedLayers":2322,"bossDamage":2840562,"dmgDealt":16880657,"gold":174,"itemsEquipped":28,"kills":280,"leaks":6,"lpLost":5,"merges":4,"perfectRounds":11,"refreshes":40},
"lineup": ["2_17_a@12,8[3_08_e_a+4_02_e_a]","5_03_a@11,8[3_02_e_a+3_10_e_a]","1_10_b@10,4[4_11_e_a+2_01_e_b]","4_11_a@10,5[2_03_e_b+3_10_e_b]","5_06_a@10,8[3_01_e_b+5_02_e_a]","3_16_a@9,3[2_03_e_b+3_09_e_a]","6_20_a@9,6[1_05_e_a+4_08_e_b]","1_06_b@9,9[3_08_e_b+1_05_e_b]"],
"bonds": ["arcaneShip 0/302","deputShip 1/328","egirShip 0/300","emptyShip 0/300","indomShip 1/326","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 0/309","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/314","soloShip 1/318","steadShip 2/384","suntShip 1/310","swiftShip 0/300","victoriaShip 1/342","visiShip 0/306","yanShip 0/310"]
}
+26 -26
View File
@@ -2546,14 +2546,14 @@
"round": 10,
"seed": 3609857679,
"waveId": "act1autochess_h03",
"time": 161.57,
"ticks": 4847,
"time": 178.03,
"ticks": 5341,
"reason": "cleared",
"killed": 59,
"total": 59,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 64,
"rngDraws": 61,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {
@@ -2563,26 +2563,26 @@
"leaks": 0,
"leaksUncounted": 0,
"coins": 8,
"dmg": 704844,
"dmg": 706144,
"bossDmg": 0,
"heal": 28019,
"deaths": 3,
"heal": 27206,
"deaths": 6,
"layerGains": {"preciShip":80,"sargonShip":108}
}
},
"units": [
["p1",1,"op","chess_char_4_17_b",2,"none",4566.9,1,3,1,1288,0,0,2,0],
["p1",2,"op","chess_char_4_18_b",2,"none",6783,1,5,0,7191,0,0,3,1],
["p1",3,"op","chess_char_4_23_b",1,"uniequip_004_gvial2",3667.3,1,7,1,7785,649,649,3,2],
["p1",4,"op","chess_char_4_19_b",1,"none",2525.9,1,10,1,8997,1531,0,0,4],
["p1",5,"op","chess_char_4_22_b",1,"uniequip_002_svrash",3339.7,1,11,3,40105,9401,4764,46,1],
["p1",6,"op","chess_char_5_01_a",2,null,1722.46,1,40,4,78323,9642,6250,66,1],
["p1",7,"op","chess_char_4_16_b",1,"none",507.1,1,71,7,97453,7002,1741,95,6],
["p1",8,"op","chess_char_4_13_b",2,"uniequip_004_gnosis",2553.2,1,8,4,31086,0,0,39,3],
["p1",9,"op","chess_char_4_20_b",1,"none",1844.7,1,9,21,131494,1023,0,64,3],
["p1",10,"op","chess_char_4_24_b",2,"none",2571.4,1,12,10,154430,6521,646,37,3],
["p1",11,"op","chess_char_4_25_b",1,"uniequip_002_cetsyr",2172.3,1,4,0,19289,0,0,0,3],
["p1",12,"op","chess_char_5_02_a",1,null,1570.4,1,6,7,127404,903,13969,30,3],
["p1",1,"op","chess_char_4_17_b",2,"none",4566.9,1,3,0,0,0,0,0,0],
["p1",2,"op","chess_char_4_18_b",2,"none",6783,1,5,1,3204,0,0,2,0],
["p1",3,"op","chess_char_4_23_b",1,"uniequip_004_gvial2",3667.3,1,7,1,7785,649,453,3,2],
["p1",4,"op","chess_char_4_19_b",1,"none",2525.9,1,10,0,22404,1531,0,2,5],
["p1",5,"op","chess_char_4_22_b",1,"uniequip_002_svrash",3339.7,1,11,4,45135,11366,7678,52,1],
["p1",6,"op","chess_char_5_01_a",2,null,0,0,74,0,33180,11271,3600,57,0],
["p1",7,"op","chess_char_4_16_b",1,"none",0,0,70,7,103396,7566,1798,97,6],
["p1",8,"op","chess_char_4_13_b",2,"uniequip_004_gnosis",2553.2,1,8,4,29700,0,0,39,3],
["p1",9,"op","chess_char_4_20_b",1,"none",1844.7,1,9,23,148474,1023,0,68,3],
["p1",10,"op","chess_char_4_24_b",2,"none",2571.4,1,12,11,171104,6606,574,44,4],
["p1",11,"op","chess_char_4_25_b",1,"uniequip_002_cetsyr",2172.3,1,4,1,10365,0,0,0,3],
["p1",12,"op","chess_char_5_02_a",1,null,1570.4,1,6,7,131396,633,13103,31,5],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
@@ -2592,17 +2592,17 @@
"enemy_1112_emppnt_2": [1,1,0,0,18432],
"enemy_1183_mlasrt": [1,1,0,573,7488],
"enemy_1249_lysdb_2": [1,1,0,0,6912],
"enemy_1267_nhpbr_2": [33,33,0,9577,209088],
"enemy_1270_nhstlk_2": [12,12,0,6539,221184],
"enemy_1272_nhtank_2": [6,6,0,7102,103680],
"enemy_1267_nhpbr_2": [33,33,0,8565,209088],
"enemy_1270_nhstlk_2": [12,12,0,6590,221184],
"enemy_1272_nhtank_2": [6,6,0,12872,103680],
"enemy_1329_cbshld_2": [1,1,0,1824,14976],
"enemy_1512_mcmstr": [1,1,0,5044,92160],
"enemy_1512_mcmstr": [1,1,0,4209,92160],
"enemy_9008_acbunn": [1,1,0,1373,13824]
},
"hooks": [1,76,4847,498,498,1282,1296,2030,65,62,30,27,16,17319,1994,40,59,0,1,70,51,1,7],
"events": {"atk":626,"bounty":4,"deploy":15,"die":62,"dmg":1130,"engage":10,"fx":198,"heal":161,"layer":51,"skill":57,"spawn":73,"status":274},
"fx": {"aoe":4,"bombard":5,"bombardShell":5,"burst":1,"coldWind":3,"devour":3,"dodge":7,"explode":26,"extraAttack":15,"garrison":27,"garrisonGrant":1,"iceSpike":3,"link":3,"mote":34,"overload":2,"revive":1,"reweave":3,"rockfall":1,"shieldBreak":2,"sleepGuard":3,"snow":1,"stone":1,"swordStorm":6,"telegraph":2,"zone":39},
"snaps": "875d082b 2d4fe188 7fcebf0f 0527d505 bb63a101 48180fae 265505ed 9e4dc1bc 9ba5f9b2 78ac7755 8a05d44c 142de75f 53a7aa5d 12ee3255 c9643b48 a3e02d33 beb312d1"
"hooks": [1,77,5341,508,508,1296,1311,2474,68,65,32,30,0,18486,2146,40,59,0,1,75,51,1,6],
"events": {"atk":620,"bounty":4,"deploy":16,"die":65,"dmg":1143,"engage":10,"fx":192,"heal":162,"layer":51,"skill":62,"spawn":73,"status":288},
"fx": {"aoe":5,"bombard":5,"bombardShell":5,"burst":1,"coldWind":3,"devour":4,"dodge":6,"explode":26,"extraAttack":9,"garrison":27,"garrisonGrant":1,"iceSpike":3,"link":3,"mote":33,"overload":2,"revive":1,"reweave":3,"shieldBreak":1,"sleepGuard":5,"snow":1,"swordStorm":6,"telegraph":2,"zone":40},
"snaps": "875d082b 2d4fe188 7fcebf0f bdea2189 9cf65199 18d2f7a8 90cd310a 78706492 7661fcef ac8a9f73 15508efb 81a2f945 80f95bc4 a3d670f4 2e7c868e 3ff4d1ff 771c9042 584ea9ff"
},
"roster-043": {
"about": "pass 2 band 5: chess_char_5_08_a chess_char_5_11_a chess_char_5_05_a chess_char_5_06_a chess_char_5_13_a chess_char_5_14_a chess_char_5_07_a chess_char_5_17_a chess_char_5_03_a chess_char_5_10_a chess_char_5_12_a chess_char_5_15_a",
+2 -2
View File
@@ -88,8 +88,8 @@ test('终极 Final Assault (+200 layers per active bond): one bloodPoint pool fo
for (const k of MUL_STATS) assert.ok(!(k in b.mods), `${u.name}: ${b.key} is a 直接乘算 bonus, not ${k} (${JSON.stringify(b.mods)})`);
}
const { add, mul } = aggregateMods(u.buffs);
const want = Math.max(0, (u.base.atk + (add.atkFlat ?? 0)) * Math.max(0, 1 + (add.atkPct ?? 0)) * (mul.atkMul ?? 1));
assert.ok(Math.abs(u.s.atk - want) <= 1e-6 * Math.max(1, want), `${u.name}: ATK = (base + flat) × (1 + Σ%) × Π(提升至/runes)`);
const want = Math.max(0, ((u.base.atk + (add.atkFlat ?? 0)) * Math.max(0, 1 + (add.atkPct ?? 0)) + (add.atkFinal ?? 0)) * (mul.atkMul ?? 1));
assert.ok(Math.abs(u.s.atk - want) <= 1e-6 * Math.max(1, want), `${u.name}: ATK = ((base + flat) × (1 + Σ%) + 最终加算) × Π(提升至/runes)`);
}
}
assert.ok(ops >= 12 && withBonds >= 8, `the bots' lineups carry bond / item bonuses (${withBonds} of ${ops})`);
+1 -1
View File
@@ -122,7 +122,7 @@ test('乌尔比安 S3: the 阿戈尔 devour gains stay through the 【移动】
const gain = () => u.buffs.find((x) => x.key === 'bond:egir:devour');
assert.ok(gain(), 'he devoured the piece in front of him at the battle start');
const mods = { ...gain().mods }, atk0 = u.s.atk, block0 = u.s.blockCnt;
assert.equal(mods.atkFlat, 1000);
assert.equal(mods.atkFinal, 1000);
u.skill.gainSp(1000);
assert.ok(h.runUntil(() => u.skill.active, 2));
assert.deepEqual([u.tileR, u.tileC], [10, 7], 'moved');