diff --git a/docs/SIM.md b/docs/SIM.md index 192a4bbc..85176b02 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -95,7 +95,9 @@ where it fell for the `kill` / `death` handlers. "倒地干员所在地块视为 tile and `isReservedTile` reports it, so every automatic picker skips it. The rule covers every 退场 (GitHub #60): an operator forced out by its own effects (`retreat` reason `'retreat'`: 史尔特尔's 余烬, 耀骑士临光 S2, 骑士戒律 + 竞技旗, 伊内丝 S3; `'merchant'`: a 商人 that cannot pay) lies down and comes back the same way — still no kill (its death reason is -not `'killed'`: no 被击倒 effects, 阿戈尔 or knock-down count), but 不屈 rolls on it (PRTS 盟约记录 不屈 修正 "被击倒、撤退、 +not `'killed'`: no 被击倒 effects, 阿戈尔 or knock-down count — except Touch's 超脱 on a `dying` exit, a knock-out put off +by the operator's own effect that plays its death animation: 史尔特尔's 余烬, 骑士戒律 + 竞技旗; PRTS Touch(卫戍协议) 备注 +"部分有死亡动画的强制撤退(如史尔特尔的天赋效果)也能触发这一天赋"), but 不屈 rolls on it (PRTS 盟约记录 不屈 修正 "被击倒、撤退、 切换<替身>与<本体>时": a hit redeploys it at once, free, where it lies; addon/battle.js); only the 突袭 retreat (`'raid'`, redeployed at once on its landing tile — no 不屈 roll) and permanent removals leave nothing. @@ -732,7 +734,7 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO | `dollSwitch` | `{ unit, reason, done }` | content switches a 傀儡师 to its <替身> now (归溟幽灵鲨 S2 "技能结束后立刻切换为<替身>": no lethal HP loss); its trait does it unless it already is one or is not on the field, and sets `done` | | `dollSwap` | `{ unit, form }` | a 傀儡师 starts a switch — to its <替身> (`form` `'doll'`) or back to its <本体> (`null`); not when it is knocked out as the 替身 (不屈 rolls on it: "切换<替身>与<本体>时") | | `kill` | `{ killer, victim }` | victim HP reached 0 (a handler may revive by restoring HP) | -| `death` | `{ unit, reason:'killed'|'leak'|'retreat'|'merchant'|'expired'|'forcedExit', killer }` | unit removed (`'forcedExit'`: an operator entering 联防 knocked out, §1.1) | +| `death` | `{ unit, reason:'killed'|'leak'|'retreat'|'merchant'|'expired'|'forcedExit', killer, dying }` | unit removed (`'forcedExit'`: an operator entering 联防 knocked out, §1.1; `dying`: a `retreat` that plays the death animation — 史尔特尔's 余烬, 骑士戒律 + 竞技旗) | | `skillStart` / `skillEnd` | `{ unit, skill, reason }` | mutate `skill.ammoLeft` / `skill.timeLeft` in skillStart (bullets added there raise `skill.ammoMax`, the ammo bar's full mark) | | `ammoUsed` | `{ unit, left, skill }` | per ammo consumed | | `spGain` | `{ unit, amount, reason:'time'|'attack'|'hurt'|'init'|…, skill }` | mutable `amount` (time gains fire every tick) | @@ -777,7 +779,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged | `addLayers(playerId, bondId, n, reason, {source})`, `addCoins(playerId, n)` | layers are a no-op when `flags.layerGainsEnabled` is false (unite/boss); a gain adds at most the room left under `BOND_LAYER_CAP` (999, shared/constants.js `layerGainRoom`: the client's `AddBondCount` min(L + n, 999)) on the live copy — or, without one, on the battle's own gains — and returns what it added (0 at the cap: no hook, no event) | | `getPlayer(playerId)` | `{ playerId, seat, side, colOffset, mirror, dir (default unit direction: RIGHT, mirrored side LEFT), facing (its sign), bonds (live copy, layers updated by addLayers), bandId, playerEffects, lpForBoss, dp, units }` | | `mapTile(ps, row, col, abs?)` / `mapDir(ps, dir, abs?)` | board → field tile / direction of a player (the FA right-side mirror) | -| `addDp(playerId, n)`, `retreat(unit, {reason, permanent})`, `relocate(unit, r, c)` | `relocate` only changes the tile (state kept, no event) | +| `addDp(playerId, n)`, `retreat(unit, {reason, permanent, dying})`, `relocate(unit, r, c)` | `relocate` only changes the tile (state kept, no event) | | `moveRedeploy(unit, r, c, { clearSp })` | a 【移动】 (PRTS 术语释义: "不退场,以当前血量在目标位置部署", a special retreat + redeploy): `relocate`'s checks (false = refused, nothing changed), then a new deployment (`deploySeq` / `aggroSeq` / `deployedAt`) and `deploy {initial:false, move:true}` (deploy effects fire again); no `die` / `death`, timer or cost (不屈 / 阿戈尔's revive never see it); HP, buffs and a running skill are kept (owner's deviation, DESIGN §22.3); `clearSp` empties the SP before the deploy handlers run — 乌尔比安 S3's move (kits/ops/chess_char_5_05-ulpia.js) and 【返回】 (`clearSp`; also the 从不混淆的方向 fallback, content/tokens.js) | | `redeploy(unit, { free=true, tile, keepSp })` | immediate (re)deployment of a dead/retreated ally (full HP, `deploy {initial:false}`); `free: false` pays `base.cost` DP (refused without it); without `tile` it lands on the unit's rest tile (`restTile`: where a knocked-out operator lies, else home); `tile: [r, c]` lands on that tile once (home unchanged; refused when off-rect, occupied or a knocked-out operator's tile, no fallback); `keepSp` keeps SP/charges (保留技力), restored before `deploy` fires — 突袭 raids, 阿戈尔 / 不屈 revives where the unit lies | | `refreshRange(unit)`, `setExtraRange(unit, keys)`, `rangeChanged(unit)` | rebuild the ranges after changing `unit.rangeGrid` (流形 copies); extra targetable tiles (absolute keys; merged into every later rebuild until set again; `null` clears; never in `baseRangeKeys`): 蕾缪安 wanted, 维娜 S3 | diff --git a/server/sim/battle/deploy.js b/server/sim/battle/deploy.js index 9b51e715..13df9fa1 100644 --- a/server/sim/battle/deploy.js +++ b/server/sim/battle/deploy.js @@ -86,13 +86,15 @@ export class BattleDeploy { /** * Withdraw an ally without a kill (it may redeploy after its respawn time): an operator lies down where it stood and * comes back there (isDown, GitHub #60) — unless `permanent`, or the 突袭 retreat ('raid') that redeploys it at once. + * `dying`: a forced exit that plays the operator's death animation — a knock-out put off by its own effect (史尔特尔's + * 余烬, 骑士戒律 + 竞技旗); the `death` hook carries it (Touch's 超脱 counts such an exit: content/tokens.js). */ - retreat(unit, { reason = 'retreat', permanent = false } = {}) { + retreat(unit, { reason = 'retreat', permanent = false, dying = false } = {}) { if (!unit || !unit.alive || unit.side !== 'ally') return; - this._remove(unit, reason, null, permanent); + this._remove(unit, reason, null, permanent, dying); } - _remove(unit, reason, killer = null, permanent = false) { + _remove(unit, reason, killer = null, permanent = false, dying = false) { unit.alive = false; unit.removeReason = reason; unit.deployed = false; @@ -145,7 +147,7 @@ export class BattleDeploy { } // the reason ('killed' | 'retreat' | 'expired' | …) lets the client keep the knock-down sound for real knock-outs if (reason !== 'leak') this._ev(['die', unit.id, reason]); - if (this._hooks.death) this.emit('death', { unit, reason, killer }); + if (this._hooks.death) this.emit('death', { unit, reason, killer, dying: !!dying }); if (unit.removed) this._toRelease.push(unit); } diff --git a/server/sim/battle/tiles.js b/server/sim/battle/tiles.js index a93ca0ae..23da725c 100644 --- a/server/sim/battle/tiles.js +++ b/server/sim/battle/tiles.js @@ -185,9 +185,10 @@ export class BattleTiles { * decision of 2026-10-04): knocked out ('killed'), entering the battle knocked out (FORCED_EXIT, 联防) and forced out * by its own effects ('retreat': 史尔特尔's 余烬, 耀骑士临光 S2, 骑士戒律 + 竞技旗, 伊内丝 S3; 'merchant': a 商人 that cannot * pay) — except the 突袭 retreat ('raid', redeployed at once on its landing tile); not removed for good, after it was - * deployed. A forced exit stays no kill: its 'die' / `death` reason is not 'killed' (no 被击倒 effect, 不屈, 阿戈尔, no - * knock-down count). The client keeps its model on that tile knocked down with a redeploy countdown (b.snap `down`, - * render/units.js); summons, devices and enemies simply leave. + * deployed. A forced exit stays no kill: its 'die' / `death` reason is not 'killed' (no 被击倒 effect, 阿戈尔, no + * knock-down count — but 不屈 rolls on it, and Touch's 超脱 counts a `dying` one: 史尔特尔's 余烬, 骑士戒律 + 竞技旗). The + * client keeps its model on that tile knocked down with a redeploy countdown (b.snap `down`, render/units.js); summons, + * devices and enemies simply leave. */ isDown(u) { return !!u && u.side === 'ally' && u.kind === 'op' && !u.alive && !u.removed && u.removeReason !== 'raid' diff --git a/server/sim/content/items/battle.js b/server/sim/content/items/battle.js index 9ec6277d..3e3b1602 100644 --- a/server/sim/content/items/battle.js +++ b/server/sim/content/items/battle.js @@ -739,6 +739,7 @@ const BY_ITEM = { }, // 骑士戒律 (卡西米尔): skill start ⇒ 20 s aura: enemies in range ASPD ×attack_speed, move ×move_speed; // + 卡西米尔竞技旗: during the (timed) skill ATK +atk, lethal damage does not retreat it — it retreats when the skill ends + // ("受到致命伤害时不撤退,技能结束后退场": a knock-out put off, so a `dying` retreat — Touch's 超脱 counts it [ASSUMED]) chess_item_6_10_e(battle, u, rec, S) { const p = bp(rec, 'act2autochess_equip_acarm119_global_buff'); const a = bp(rec, 'act2autochess_equip_acarm119_ability'); @@ -763,7 +764,7 @@ const BY_ITEM = { battle.removeBuff(u, comboKey); if (doomed && c.reason !== 'death') { doomed = false; - battle.after(0, () => { if (u.alive) battle.retreat(u, { reason: 'retreat' }); }); + battle.after(0, () => { if (u.alive) battle.retreat(u, { reason: 'retreat', dying: true }); }); } }); S.on('fatal', (c) => { diff --git a/server/sim/content/kits/ops/chess_char_5_07-surtr.js b/server/sim/content/kits/ops/chess_char_5_07-surtr.js index adf8f0f7..500a4d0c 100644 --- a/server/sim/content/kits/ops/chess_char_5_07-surtr.js +++ b/server/sim/content/kits/ops/chess_char_5_07-surtr.js @@ -66,7 +66,8 @@ export default { // noHeal (no heal pick, no heal from others) + healFree (her own heals too; S3's start heal "无视禁疗"), shown as // the status 'healFree' until she leaves. "强制退出战场视为撤回干员": a retreat (Battle.retreat drops the buff) — she // lies down where she stood and redeploys there, like every operator that leaves the field (PRTS 卫戍协议/帮助 - // "干员退场后…原地留下一个“倒地干员”…自动部署至该位置"; Battle.isDown, GitHub #60). + // "干员退场后…原地留下一个“倒地干员”…自动部署至该位置"; Battle.isDown, GitHub #60). With her death animation + // (`dying`): PRTS Touch(卫戍协议) 超脱 备注 counts it as a knock-out ("如史尔特尔的天赋效果"). const wait = num(t1['surtr_t_2[withdraw].interval'], 8); battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.ember = false; }, { owner: unit }); battle.on('fatal', (c) => { @@ -77,7 +78,7 @@ export default { const dep = unit.deploySeq; battle.addBuff(unit, { key: 'surtr:ember', status: 'healFree', flags: { noHeal: true, healFree: true } }); battle.fx('ember', { x: unit.x, y: unit.y, id: unit.id }); - battle.after(wait, () => { if (unit.alive && unit.deploySeq === dep) battle.retreat(unit, { reason: 'retreat' }); }, { owner: unit }); + battle.after(wait, () => { if (unit.alive && unit.deploySeq === dep) battle.retreat(unit, { reason: 'retreat', dying: true }); }, { owner: unit }); }, { owner: unit, priority: -60 }); } }, ], diff --git a/server/sim/content/kits/ops/standin-acmedc.js b/server/sim/content/kits/ops/standin-acmedc.js index 8f28985e..5b047fdc 100644 --- a/server/sim/content/kits/ops/standin-acmedc.js +++ b/server/sim/content/kits/ops/standin-acmedc.js @@ -19,16 +19,16 @@ export default { // `attack@max_target` targets, every 医疗 operator of hers ATK +`attack@atk` herself included [ASSUMED: her own // player's operators, as 余's "全场所有干员"; summons not]. // S3 恳切福音 (40 s): content/tokens.js touchGospel — the one implementation the 外勤医疗 strategy's Touch runs too - // (5-2 grid, ATK +atk, 2 targets, an extra heal of 30 % of the main one on the target or an orthogonal neighbour, - // ×heal_scale on allies at ≤ hp_ratio HP). + // (5-2 grid, ATK +atk, 2 targets, ×heal_scale on allies below hp_ratio HP, and an extra heal of 30 % of the main + // heal's base on the main target or an orthogonal neighbour — the lowest HP ratio, a full-HP one included — which + // takes the ×heal_scale of its own recipient: PRTS 技能3 备注). // Talents 攫升 / 超脱: content/tokens.js mapCharTalents (the strategy's Touch too); the module's 超脱 upgrade (8 SP at // module Lv3) comes with the record's talents. // Module PHY-X (elite, trait addition): "治疗生命值低于50%的友方单位时治疗量提升15%" — every heal of hers on an ally // below `hp_ratio` HP (strictly: "低于") ×`heal_scale` (the trait blackboard). With S3's ×1.35 / ×1.40 it // multiplies (×1.61 at Lv7): both are `heal_scale` blackboards, the 治疗倍率, and PRTS 游戏数据基础 §倍率 says "同种 - // 倍率间叠乘" — no Touch-specific note or buff template exists. It runs before S3's hook, so S3's 30 % extra heal is - // a share of the main heal with both bonuses and, like S3's ×1.35 / ×1.40 in touchGospel, never multiplies that - // share again [ASSUMED]. + // 倍率间叠乘" — no Touch-specific note or buff template exists. S3's extra heal is a heal of hers like any other: its + // recipient below `hp_ratio` gets the ×1.15 too (its base is taken before either bonus). // Triggers (data, nothing set here): S1 DEFAULT (an injured ally in her range); S2 / S3 ACTIVE_RANGE — the owner's // rule of 2026-10-05 read for a heal skill: an injured ally inside the running 3-10 / 5-2 range casts. char_613_acmedc: (bb, chess, def) => { @@ -79,8 +79,7 @@ export default { install(battle, unit) { if (phyScale !== 1 && phyRatio > 0) { battle.on('heal', (ctx) => { - // (S3's extra heal is a share of the main heal, which already carries it: touchGospel's `touchExtra`) - if (ctx.source !== unit || !ctx.target || ctx.opts?.regen || unit.mem.touchExtra) return; + if (ctx.source !== unit || !ctx.target || ctx.opts?.regen) return; if (ctx.target.hpRatio < phyRatio - 1e-9) ctx.amount *= phyScale; }, { owner: unit, priority: 10 }); } diff --git a/server/sim/content/tokens.js b/server/sim/content/tokens.js index 2efd6323..c679957e 100644 --- a/server/sim/content/tokens.js +++ b/server/sim/content/tokens.js @@ -1338,12 +1338,26 @@ export function spawnYanyou(battle, playerId, { atk, hp, atkMul = 1, dmgTakenMul // --------------------------------------------------------------------------------------------------------------- // band map characters (预备干员-医疗 / Touch) +/** + * Exits that give Touch's 超脱 its SP (PRTS Touch(卫戍协议) 第二天赋 备注 "部分有死亡动画的强制撤退(如史尔特尔的天赋效果)也能触发这一 + * 天赋"): a knock-out ('killed') and a forced exit that plays the death animation — Battle.retreat `dying`, a knock-out put + * off by the operator's own effect: 史尔特尔's 余烬 (the PRTS example) and 骑士戒律 + 竞技旗 ("受到致命伤害时不撤退,技能结束后 + * 退场" [ASSUMED: the same kind of exit]). Not the exits a skill plans — 耀骑士临光 S2's "技能结束后自动撤退", 伊内丝 S3's "放置 + * 一个影哨后离场" — nor the 商人's "不足时自动撤退" ('merchant'), the 突袭 jump ('raid'), the 联防 setup (FORCED_EXIT) or a + * summon's end ('expired') [ASSUMED: plain 撤退 without a death animation]. A battle runs on its own — the player has no + * 撤退 command in it — so none of these is a player's own retreat. + */ +const touchExitCounts = (ctx) => ctx.reason === 'killed' || (ctx.reason === 'retreat' && !!ctx.dying); + /** * Talents of the band map characters (data talents of the character record) — and of the Touch 补位 stand-in, whose kit * (kits/ops/standin-acmedc.js) uses this one implementation, the module's 超脱 upgrade (8 SP) coming with the record: * plain stat talent "攻击力+4%" (预备干员-医疗 攻击提升) → persistent ATK/DEF/HP/ASPD buff - * 攫升 "治疗目标时使其获得3点技力" (Touch) → every heal by the character gives the healed unit `sp` SP - * 超脱 "攻击范围内的友方干员被击倒时获得5点技力" (Touch) → an allied operator knocked out on a tile of its range: +`sp` SP + * 攫升 "治疗目标时使其获得3点技力" (Touch) → every heal the character outputs gives its target `sp` SP — PRTS 备注 "本天赋只需 + * Touch输出治疗便能触发(无需实际产生治疗量)": herself too, and a heal that restores nothing (a full-HP target: S3's + * extra heal); not a device or a unit without a skill, not her own HP-regeneration tick (no heal she outputs) + * 超脱 "攻击范围内的友方干员被击倒时获得5点技力" (Touch) → an allied operator knocked out (or forced out dying: + * touchExitCounts) on a tile of its range: +`sp` SP */ export function mapCharTalents(def) { const out = []; @@ -1355,7 +1369,7 @@ export function mapCharTalents(def) { out.push({ install(battle, unit) { battle.on('heal', (ctx) => { const tg = ctx.target; - if (ctx.source !== unit || !tg || tg === unit || tg.kind === 'device' || !tg.skill || !(ctx.amount > 0)) return; + if (ctx.source !== unit || ctx.opts?.regen || !tg || tg.kind === 'device' || !tg.skill || tg.skill.noSkill) return; tg.skill.gainSp(sp, 'talent'); }, { owner: unit, priority: -200 }); } }); @@ -1363,7 +1377,7 @@ export function mapCharTalents(def) { out.push({ install(battle, unit) { battle.on('death', (ctx) => { const d = ctx.unit; - if (ctx.reason !== 'killed' || !unit.alive || !unit.deployed || !unit.skill || !d || d === unit || d.kind !== 'op' || d.ownerId !== unit.ownerId) return; + if (!touchExitCounts(ctx) || !unit.alive || !unit.deployed || !unit.skill || !d || d === unit || d.kind !== 'op' || d.ownerId !== unit.ownerId) return; if ((unit.rangeKeySet || new Set(unit.rangeKeys || [])).has(d.tileR * COLS + d.tileC)) { unit.skill.gainSp(sp, 'talent'); battle.fx('spGain', { x: unit.x, y: unit.y, id: unit.id, n: sp }); @@ -1391,11 +1405,19 @@ function reserveMedicKit(bb, raw, def) { } /** - * Touch 恳切福音 (skchr_acmedc_3): +ATK, 2 heal targets in the skill range, ×heal_scale on allies ≤ hp_ratio, +addition + * Touch 恳切福音 (skchr_acmedc_3): +ATK, 2 heal targets in the skill range, ×heal_scale on allies below hp_ratio, +addition * heal — one implementation for the 外勤医疗 map character (touchKit) and the Touch 补位 stand-in (kits/ops/ * standin-acmedc.js). `skill` = the skill record (`rangeGrid`, `duration`) of blackboard `bb`. Returns `{ skill: * SkillSpec, install(battle, unit) }`: `install` adds the heal boost, which acts while the unit's skill runs — install it - * on a unit whose skill is this one only. + * on a unit whose skill is this one only. PRTS Touch(卫戍协议) 技能3 and its 备注: + * - "对生命值低于一半的友方单位治疗量提高为原来的130%": strictly below hp_ratio (PRTS 修正: 低于, not the old 不高于); + * - "并额外治疗一次目标或目标相邻1个友方单位,治疗量为主目标的30%": once per heal action, at the main (first = most + * injured) target's hit; 30 % of that heal's base (ATK × its scales, before the ×heal_scale the main target may get); + * - 备注 "额外治疗范围 x-5…优先选择生命比例更低的单位治疗(可治疗生命值已满单位)": the main target or one of its 4 + * orthogonal neighbours, the lowest HP ratio first, a full-HP one included [ASSUMED: on equal ratios the main target, + * then the battle's ally order]; never a 禁疗 / 孤立 / 无法被友方治疗 unit (no heal can pick it); + * - 备注 "额外治疗可触发技能后半段的治疗量提高效果": the extra heal is a heal of its own, so its recipient gets the ×heal_scale + * when it is below hp_ratio (and the stand-in's PHY-X ×1.15), and its target gets 攫升's SP. */ export function touchGospel(bb, skill) { const hpRatio = num(bb.hp_ratio, 0), boost = num(bb.heal_scale, 1), extra = num(bb['attack@addition_heal_scale'], 0); @@ -1409,28 +1431,24 @@ export function touchGospel(bb, skill) { targeting, onHit({ battle, unit, target, heal }) { if (!(extra > 0) || !target || !(heal > 0)) return; - // "每次治疗2个目标,并额外治疗一次…治疗量为主目标的30%": one extra heal per heal action, for the main - // (first = most injured) target — onHit runs once per healed target + // onHit runs once per healed target: the extra heal comes with the first (main) one of the action const n = unit.stats.attacks; if (unit.mem.touchExtraAt === n) return; unit.mem.touchExtraAt = n; - let best = target.hp < target.s.maxHp - 1e-6 ? target : null; - for (const a of battle.alliesInRadius(target.x, target.y, 1, unit.ownerId)) { - if (a === target || a.hp >= a.s.maxHp - 1e-6 || a.s.flags.noHeal || a.profile?.noHeal) continue; // (禁疗 / 孤立) + let best = target; + for (const a of battle.alliesInRadius(target.x, target.y, 1)) { + if (a === target || !battle.allySelectable(a, unit) || a.s.flags.noHeal || a.profile?.noHeal) continue; if (Math.abs(a.tileR - target.tileR) + Math.abs(a.tileC - target.tileC) !== 1) continue; - if (!best || a.hpRatio < best.hpRatio) best = a; + if (a.hpRatio < best.hpRatio - 1e-9) best = a; } - // 30 % of the main target's heal as it was applied (×heal_scale when that target was at ≤ hp_ratio) - const main = unit.mem.touchMainHeal && unit.mem.touchMainHeal.target === target ? unit.mem.touchMainHeal.amount : heal; - unit.mem.touchExtra = true; // the extra heal is a share of the main heal: no second ×heal_scale - try { battle.heal(unit, best ?? target, main * extra); } finally { unit.mem.touchExtra = false; } + // `heal` = the main heal's base (ai.js doHeal: ATK × scales, before the heal pipeline and the ×heal_scale) + battle.heal(unit, best, heal * extra); }, }, install(battle, unit) { battle.on('heal', (ctx) => { - if (ctx.source !== unit || !unit.skill?.active || unit.mem.touchExtra) return; - if (boost !== 1 && hpRatio > 0 && ctx.target.hpRatio <= hpRatio + 1e-9) ctx.amount *= boost; - unit.mem.touchMainHeal = { target: ctx.target, amount: ctx.amount }; + if (ctx.source !== unit || ctx.opts?.regen || !unit.skill?.active) return; + if (boost !== 1 && hpRatio > 0 && ctx.target.hpRatio < hpRatio - 1e-9) ctx.amount *= boost; }, { owner: unit }); }, }; diff --git a/test/content/standin_acmedc.test.js b/test/content/standin_acmedc.test.js index a359c3a2..53279e5c 100644 --- a/test/content/standin_acmedc.test.js +++ b/test/content/standin_acmedc.test.js @@ -24,6 +24,7 @@ const DEFS = { test_tank2_a: chessRec({ id: 'test_tank2_a', profession: 'TANK', stats: { maxHp: 100000, atk: 1, def: 0 }, skill: null }), // SP only from 攫升: an attack-SP skill and no enemy to attack test_sp_a: chessRec({ id: 'test_sp_a', profession: 'TANK', stats: { maxHp: 100000, atk: 1, def: 0 }, skill: { spType: 'INCREASE_WHEN_ATTACK', spCost: 1000, initSp: 0 } }), + test_sp2_a: chessRec({ id: 'test_sp2_a', profession: 'TANK', stats: { maxHp: 1000, atk: 1, def: 0 }, skill: { spType: 'INCREASE_WHEN_ATTACK', spCost: 1000, initSp: 0 } }), test_medic_a: chessRec({ id: 'test_medic_a', profession: 'MEDIC', subProfessionId: 'physician', stats: { atk: 300 }, skill: null, rangeGrid: [[0, 0]] }), }, }; @@ -97,9 +98,9 @@ test('S3 恳切福音: cast with an injured ally inside the 5-2 only (no heal be h.run(0.5); const first = h.heals.filter((x) => x.t === h.heals[0].t); assert.ok(first[0].t >= cast, `${id}: the first heal comes with the cast`); - assert.deepEqual(first.map((x) => x.uid), [2, 2], `${id}: the main heal on the far ally, then the extra on it (its neighbours are full)`); + assert.deepEqual(first.map((x) => x.uid), [2, 2], `${id}: the main heal on the far ally, then the extra on it (the lowest HP ratio of it and its neighbours)`); close(first[0].amount, u.s.atk * phy * boost, `${id}: main heal ×${boost}${phy > 1 ? ' ×1.15 PHY-X' : ''}`); - close(first[1].amount, first[0].amount * 0.3, `${id}: extra heal = 30 % of the main one`); + close(first[1].amount, u.s.atk * 0.3 * phy * boost, `${id}: extra heal = 30 % of the main heal's base, its still-low recipient's bonuses on it`); // two injured allies: both healed per action h.b.loseHp(near, near.hp * 0.2); const n0 = h.heals.length; @@ -121,6 +122,41 @@ test('S3 恳切福音: cast with an injured ally inside the 5-2 only (no heal be } }); +test('S3 extra heal (PRTS 备注): 30 % of the main heal\'s base, its own recipient\'s ×heal_scale (and PHY-X); the lowest HP ratio of the main target and its 4 neighbours; exactly half HP is not 「低于」', () => { + for (const id of ['chess_char_5_11_a', 'chess_char_6_09_b']) { + const phy = moduleOf(id) ? 1.15 : 1; + const boost = skillOf(id, S3).bb.heal_scale; + // the main target on (10,8) (inside the 5-2, outside her 3-3); its neighbour (10,9) is beyond the 5-2 (never a main target) + const firstAction = (mainRatio, nearRatio) => { + const h = battle({ id, carry: { sp: 999 } }, [['test_tank_a', 10, 8, mainRatio], ['test_tank2_a', 10, 9, nearRatio]]); + const u = h.unit(1); + assert.ok(h.runUntil(() => h.heals.length >= 2, 5), `${id}: healed`); + assert.ok(u.skill.active, `${id}: S3 running`); + const t0 = h.heals[0].t; + const act = h.heals.filter((x) => x.t === t0); + assert.equal(act.length, 2, `${id}: one main heal and one extra heal`); + checkInvariants(h.b); + return { u, main: act[0], extra: act[1] }; + }; + // main above half: no bonus; the extra goes to the neighbour below half (lower ratio), with both bonuses + let r = firstAction(0.55, 0.45); + assert.deepEqual([r.main.uid, r.extra.uid], [2, 3], `${id}: the extra on the lower-ratio neighbour`); + close(r.main.amount, r.u.s.atk, `${id}: no bonus at 55 %`); + close(r.extra.amount, r.u.s.atk * 0.3 * boost * phy, `${id}: the extra takes its own recipient's ×${boost}`); + // main at exactly half: "低于" — no ×heal_scale, no PHY-X; the extra on it (its neighbour is full), no bonus either + r = firstAction(0.5, 1); + assert.deepEqual([r.main.uid, r.extra.uid], [2, 2], `${id}: the full neighbour has the higher ratio`); + close(r.main.amount, r.u.s.atk, `${id}: exactly 50 % is not below half`); + close(r.extra.amount, r.u.s.atk * 0.3, `${id}: extra = 30 % of the base`); + // main just below half: boosted; the extra lands on it after it crossed half — 30 % of the base, not of the boosted heal + r = firstAction(0.499, 1); + assert.deepEqual([r.main.uid, r.extra.uid], [2, 2]); + close(r.main.amount, r.u.s.atk * boost * phy, `${id}: ×${boost} below half`); + assert.ok(r.extra.ratio >= 0.5, `${id}: its recipient is above half now`); + close(r.extra.amount, r.u.s.atk * 0.3, `${id}: the extra's base excludes the main heal's bonuses`); + } +}); + test('S1 慨赠: cast on an injured ally in her range; 20 s of ATK +40 % / +50 %, each heal a 40 % / 50 % chance of a second heal of the same ally', () => { for (const [id, atk, p] of [['chess_char_5_11_a', 0.4, 0.4], ['chess_char_5_11_b', 0.5, 0.5]]) { assert.deepEqual([skillOf(id, S1).bb.atk, skillOf(id, S1).bb['attack@prob'], skillOf(id, S1).duration], [atk, p, 20]); @@ -187,9 +223,9 @@ test('S2 宛如天启: cast with an injured ally inside the 3-10 only; 30 s on t assert.equal(h.unit(1).skill.activations, 0); }); -test('攫升 / 超脱 (the strategy\'s mapCharTalents): a healed unit +3 SP; an operator knocked out in her range +5 SP (+8 with PHY-X Lv3)', () => { +test('攫升 / 超脱 (the strategy\'s mapCharTalents): a healed unit +3 SP; an operator knocked out — or forced out — in her range +5 SP (+8 with PHY-X Lv3)', () => { for (const [id, sp] of [['chess_char_5_11_a', 5], ['chess_char_5_11_b', 5], ['chess_char_6_20_b', 8]]) { - const h = battle({ id }, [['test_sp_a', 10, 5, 0.6], ['test_tank_a', 9, 4, 1], ['test_tank2_a', 9, 10, 1]]); + const h = battle({ id }, [['test_sp_a', 10, 5, 0.6], ['test_tank_a', 9, 4, 1], ['test_tank2_a', 9, 10, 1], ['test_tank_a', 11, 4, 1], ['test_tank2_a', 11, 5, 1], ['test_tank_a', 9, 5, 1], ['test_tank2_a', 11, 6, 1]]); const u = h.unit(1), ally = h.unit(2); const gains = []; h.b.on('spGain', (c) => { if (c.unit === ally) gains.push([c.reason, c.amount]); }); @@ -203,6 +239,60 @@ test('攫升 / 超脱 (the strategy\'s mapCharTalents): a healed unit +3 SP; an const mid = u.skill.sp; h.b.kill(h.unit(4)); // (9,10): out of it close(u.skill.sp, mid, `${id}: not out of range`); + // PRTS 备注: a forced exit with a death animation (史尔特尔's 余烬: a `dying` retreat) counts; not a skill's planned + // retreat (耀骑士临光 S2, 伊内丝 S3: a plain 'retreat'), the 商人's automatic 撤退 ('merchant') or the 突袭 jump ('raid') + h.b.retreat(h.unit(5), { reason: 'retreat', dying: true }); // (11,4) + close(u.skill.sp, mid + sp, `${id}: 超脱 on a dying forced exit`); + h.b.retreat(h.unit(6), { reason: 'retreat' }); // (11,5) + h.b.retreat(h.unit(7), { reason: 'merchant' }); // (9,5) + h.b.retreat(h.unit(8), { reason: 'raid' }); // (11,6) + close(u.skill.sp, mid + sp, `${id}: not on a planned, a 商人's or a 突袭 retreat`); + checkInvariants(h.b); + } +}); + +test('超脱 on 史尔特尔\'s 余烬 exit (PRTS 备注 "如史尔特尔的天赋效果"): her forced exit 8 s after the lethal blow gives Touch the SP', () => { + const id = 'chess_char_5_11_a', sp = 5; + const h = battle({ id }, [['chess_char_5_07_a', 9, 4, 1]]); + const u = h.unit(1), surtr = h.unit(2); + const exits = []; + h.b.on('death', (c) => { if (c.unit === surtr) exits.push([c.reason, c.dying]); }); + h.b.dealDamage(null, surtr, { amount: 1e7, type: 'true' }); + assert.ok(surtr.alive && surtr.findBuff('surtr:ember'), '余烬: kept alive'); + const before = u.skill.sp; + const gains = []; + h.b.on('spGain', (c) => { if (c.unit === u && c.reason === 'talent') gains.push(c.amount); }); + assert.ok(h.runUntil(() => !surtr.alive, 12), 'she withdraws'); + assert.deepEqual(exits, [['retreat', true]], 'a dying forced exit'); + assert.deepEqual(gains, [sp], '超脱'); + assert.ok(u.skill.sp >= before + sp - 1e-6); + checkInvariants(h.b); +}); + +test('攫升 (PRTS 备注 "只需Touch输出治疗便能触发(无需实际产生治疗量)"): her heal on herself and a heal that restores nothing (S3\'s extra heal on a full unit) give SP too', () => { + for (const id of ['chess_char_5_11_a', 'chess_char_6_09_b']) { + // herself: injured, she heals herself (her 3-3 holds her own tile) + let h = battle({ id }, []); + let u = h.unit(1); + h.b.loseHp(u, u.hp * 0.4); + const own = []; + h.b.on('spGain', (c) => { if (c.unit === u && c.reason === 'talent') own.push(c.amount); }); + assert.ok(h.runUntil(() => h.heals.length > 0, 5), `${id}: a self-heal`); + h.step(); + assert.equal(h.heals[0].uid, 1); + assert.deepEqual(own, [3], `${id}: 攫升 on her own heal`); + checkInvariants(h.b); + // S3: the main heal fills a 1000-HP unit at 90 %, then the extra heal lands on it at full HP — still +3 SP + h = battle({ id, carry: { sp: 999 } }, [['test_sp2_a', 10, 8, 0.9]]); + u = h.unit(1); + const tgt = h.unit(2); + const got = []; + h.b.on('spGain', (c) => { if (c.unit === tgt && c.reason === 'talent') got.push(c.amount); }); + assert.ok(h.runUntil(() => h.heals.length >= 2, 5), `${id}: S3 heals`); + h.step(); + assert.ok(u.skill.active); + assert.deepEqual(h.heals.slice(0, 2).map((x) => [x.uid, x.ratio === 1]), [[2, false], [2, true]], `${id}: the extra heal on the now full unit`); + assert.deepEqual(got, [3, 3], `${id}: 攫升 for the main heal and for the extra heal that restored nothing`); checkInvariants(h.b); } }); diff --git a/test/content/tokens_devices.test.js b/test/content/tokens_devices.test.js index ea06b4e9..edfcc0d0 100644 --- a/test/content/tokens_devices.test.js +++ b/test/content/tokens_devices.test.js @@ -495,7 +495,7 @@ test('炎佑: spawnYanyou — flying ally, bond stats, 3 targets with burn + ele checkInvariants(h.b); }); -test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage slot; Touch 恳切福音 heals ×heal_scale on ≤50 % HP allies', REAL, () => { +test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage slot; Touch 恳切福音 heals ×heal_scale on allies below 50 % HP', REAL, () => { const g = guard({ stats: { maxHp: 10000 } }); const h = makeBattle({ stageId: 'act2autochess_m01', defs: { chess: { test_guard: g } }, units: [{ chessId: 'test_guard', row: 10, col: 3 }], autoFinish: false, timeLimit: 60, setup: (b) => b.on('heal', (c) => { if (c.source?.defId === TOKEN_IDS.touch) (b.mem ??= []).push({ amount: c.amount, ratio: c.target.hpRatio, active: c.source.skill.active }); }, { priority: -500 }) }); @@ -515,7 +515,7 @@ test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage sl gu.hp = 3000; h.run(4); const bb = touch.skill.bb; - const low = h.b.mem.find((x) => x.active && x.ratio <= bb.hp_ratio); + const low = h.b.mem.find((x) => x.active && x.ratio < bb.hp_ratio); assert.ok(low, 'healed a low ally'); approx(low.amount, touch.s.atk * bb.heal_scale, 1e-6, 'boosted heal'); checkInvariants(h.b); @@ -1090,7 +1090,7 @@ test('预备干员-医疗: stat talent 攻击提升 (+4 % ATK) on top of the dat approx(med.s.atk, tokDef(TOKEN_IDS.reserveMedic).stats.atk * (1.04 + 0.5), 1e-9, '治疗强化·β型 +50 %'); }); -test('Touch talents: 攫升 +3 SP to the healed unit, 超脱 +5 SP when an operator in range is knocked out; extra heal = 30 % of the main heal', REAL, () => { +test('Touch talents: 攫升 +3 SP to the healed unit, 超脱 +5 SP when an operator in range is knocked out; extra heal = 30 % of the main heal\'s base, ×heal_scale on its own low recipient', REAL, () => { const g = chessRec({ id: 'test_sp', stats: { maxHp: 10000, atk: 0, spRecovery: 0 }, skill: { spCost: 100, initSp: 0 } }); const h = makeBattle({ stageId: 'act2autochess_m01', defs: { chess: { test_sp: g } }, units: [{ chessId: 'test_sp', row: 10, col: 3, uid: 1 }, { chessId: 'test_sp', row: 11, col: 3, uid: 2 }], autoFinish: false, timeLimit: 60, setup: (b) => b.on('heal', (c) => { if (c.source?.defId === TOKEN_IDS.touch) (b.mem ??= []).push({ t: c.target.id, amount: c.amount, ratio: c.target.hpRatio }); }, { priority: -500 }) }); @@ -1105,14 +1105,17 @@ test('Touch talents: 攫升 +3 SP to the healed unit, 超脱 +5 SP when an opera h.b.dealDamage(null, b2, { amount: 1e6, type: 'true' }); assert.equal(b2.alive, false); approx(touch.skill.sp - sp0, 5, 0.1, '超脱'); - // skill: main heal (×1.5 at ≤ 50 %) + extra 30 % of it, not boosted again + // skill: main heal (×1.5 below 50 %) + an extra heal of 30 % of its base (ATK) on the lowest-ratio unit of the target and + // its neighbours — the target again, still below half after the main heal: ×1.5 on its own (PRTS 技能3 备注) h.b.mem = []; touch.skill.activate('test', { free: true }); - a.hp = 4000; + a.hp = 2000; assert.ok(h.runUntil(() => h.b.mem.length >= 2, 5)); const [main, extra] = h.b.mem; + assert.deepEqual([main.t, extra.t], [a.id, a.id]); approx(main.amount, touch.s.atk * 1.5, 1e-6, 'main heal boosted'); - approx(extra.amount, main.amount * 0.3, 1e-6, 'extra = 30 % of the main heal'); + assert.ok(extra.ratio < 0.5); + approx(extra.amount, touch.s.atk * 0.3 * 1.5, 1e-6, 'extra = 30 % of the base, boosted on its low recipient'); checkInvariants(h.b); }); diff --git a/test/golden/matches.json b/test/golden/matches.json index 30bfe729..09490e35 100644 --- a/test/golden/matches.json +++ b/test/golden/matches.json @@ -1574,7 +1574,7 @@ "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 4, "reward": 400, - "stats": {"activatedLayers":276,"bossDamage":390000,"dmgDealt":2811982,"gold":135,"itemsEquipped":23,"kills":254,"leaks":53,"lpLost":26,"merges":5,"perfectRounds":8,"refreshes":21}, + "stats": {"activatedLayers":276,"bossDamage":390000,"dmgDealt":2809891,"gold":135,"itemsEquipped":23,"kills":254,"leaks":53,"lpLost":26,"merges":5,"perfectRounds":8,"refreshes":21}, "lineup": ["6_05_a@12,3[3_08_e_a+3_05_e_a]","4_14_b@11,3[3_11_e_b+6_05_e_a]","1_17_b@10,3","6_19_a@10,4","6_17_a@10,7[4_08_e_a+1_05_e_b]","6_06_a@10,8[1_01_e_a+4_04_e_a]","3_12_a@9,4[4_04_e_b+1_02_e_b]","3_13_b@9,8[2_01_e_a+4_03_e_a]"], "bonds": ["arcaneShip 0/14","kazimierzShip 1/62","kjeragShip 0/8","preciShip 2/48","raidShip 1/105","sargonShip 1/2","skillfulShip 0/9","soloShip 0/6","suntShip 1/0","swiftShip 1/59","yanShip 0/1"] }, diff --git a/test/golden/roster.json b/test/golden/roster.json index 49a27af1..7fe87070 100644 --- a/test/golden/roster.json +++ b/test/golden/roster.json @@ -2534,12 +2534,12 @@ "rngDraws": 81, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":40,"total":47,"perfect":false,"leaks":7,"leaksUncounted":0,"coins":8,"dmg":505596,"bossDmg":0,"heal":35330,"deaths":6,"layerGains":{"visiShip":8}} + "p1": {"killed":40,"total":47,"perfect":false,"leaks":7,"leaksUncounted":0,"coins":8,"dmg":505596,"bossDmg":0,"heal":35729,"deaths":6,"layerGains":{"visiShip":8}} }, "units": [ ["p1",1,"op","chess_char_4_23_a",1,null,0,0,59,0,14296,4358,0,37,1], ["p1",2,"op","chess_char_4_01_b",1,"none",2261.07,1,9,1,12965,2729,0,125,2], - ["p1",3,"op","chess_char_4_19_a",1,null,847.88,1,65,0,1103,4894,0,25,2], + ["p1",3,"op","chess_char_4_19_a",1,null,1246.08,1,65,0,1103,4894,0,25,2], ["p1",4,"op","chess_char_4_22_a",1,null,2022,1,7,0,2567,2035,1080,66,1], ["p1",5,"op","chess_char_4_12_b",1,"none",4562.3,1,11,1,35031,22559,14453,82,5], ["p1",6,"op","chess_char_4_04_b",2,"uniequip_002_ines",2149,1,14,7,116578,7728,0,130,0], @@ -2549,7 +2549,7 @@ ["p1",10,"op","chess_char_4_24_a",2,null,1726,1,3,17,187142,0,0,41,4], ["p1",11,"op","chess_char_4_25_a",1,null,1221,1,4,5,29495,0,2741,0,3], ["p1",12,"op","chess_char_4_02_b",1,"none",2966.25,1,5,3,30378,1420,0,11,2], - ["p1","~1","token","char_613_acmedc",null,null,1314,1,13,0,0,0,17056,42,2] + ["p1","~1","token","char_613_acmedc",null,null,1314,1,13,0,0,0,17454,42,2] ], "enemies": { "enemy_1001_bigbo": [1,1,0,2149,17600], @@ -2566,7 +2566,7 @@ "hooks": [1,65,5700,1112,1112,1391,1406,25126,46,48,28,23,74,26265,88,62,47,1,1,47,2,0,5], "events": {"atk":1301,"bounty":4,"deploy":18,"die":47,"dmg":1362,"fx":173,"heal":138,"layer":2,"leak":1,"skill":50,"spawn":60,"status":296}, "fx": {"aoe":2,"beam":1,"buff":2,"devour":4,"displace":9,"dodge":5,"ember":1,"garrison":2,"garrisonGrant":1,"link":3,"mote":98,"overload":3,"reweave":3,"riposte":1,"ripple":1,"sentry":2,"shadowWeave":25,"shield":1,"snow":1,"summon":1,"wake":5,"zone":2}, - "snaps": "87882f34 f1ef51b0 5ea73197 12103e8c 818a5853 3e0a8b9b d26d3b6f d825f5c7 4ebd0c03 1c2642fe 57081f03 a2c42970 e8c109bb 4c501122 d3565c79 48a49749 f630ef04 87c6d1fa 5ce6a9f2" + "snaps": "87882f34 f1ef51b0 5ea73197 12103e8c 818a5853 3e0a8b9b d26d3b6f d825f5c7 4ebd0c03 1c2642fe 57081f03 a2c42970 e8c109bb 4c501122 d3565c79 48a49749 f630ef04 87c6d1fa fdddc647" }, "roster-042": { "about": "pass 2 band 5: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_5_01_a chess_char_4_16_b chess_char_4_13_b chess_char_4_20_b chess_char_4_24_b chess_char_4_25_b chess_char_5_02_a", diff --git a/test/golden/standins.json b/test/golden/standins.json index d637788f..e909b160 100644 --- a/test/golden/standins.json +++ b/test/golden/standins.json @@ -138,44 +138,44 @@ "time": 190, "ticks": 5700, "reason": "timeout", - "killed": 37, + "killed": 46, "total": 74, "unspawned": 0, "bossHpLeft": null, - "rngDraws": 374, + "rngDraws": 247, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":37,"total":74,"perfect":false,"leaks":37,"leaksUncounted":0,"coins":8,"dmg":331499,"bossDmg":0,"heal":49226,"deaths":12,"layerGains":{}}}, + "players": {"p1":{"killed":46,"total":74,"perfect":false,"leaks":28,"leaksUncounted":0,"coins":8,"dmg":342938,"bossDmg":0,"heal":45325,"deaths":12,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_5_08_a",2,null,0,0,6,0,4409,3444,0,59,1], - ["p1",2,"op","chess_char_5_14_a",2,null,0,0,86,16,131680,8403,0,53,6], - ["p1",3,"op","chess_char_5_19_a",0,null,0,0,90,8,38187,15644,0,91,2], - ["p1",4,"op","chess_char_5_07_a",2,null,0,0,7,2,18936,10110,0,69,2], - ["p1",5,"op","chess_char_5_13_a",2,null,1872.24,1,11,5,68168,12979,0,127,4], - ["p1",6,"op","chess_char_5_17_a",2,null,0,0,81,2,7848,5571,0,85,0], - ["p1",7,"op","chess_char_5_10_a",2,null,932,1,2,1,8617,0,0,74,2], - ["p1",8,"op","chess_char_5_11_a",2,null,1224,1,3,0,0,0,49226,72,2], - ["p1",9,"op","chess_char_5_12_a",2,null,1160,1,4,0,22812,0,0,88,2], - ["p1",10,"op","chess_char_5_15_a",2,null,0,0,85,2,8089,12138,0,87,2], - ["p1",11,"op","chess_char_5_18_a",2,null,0,0,1,1,13952,1272,0,34,1], - ["p1",12,"op","chess_char_5_20_a",2,null,742,1,91,0,8800,3202,0,43,1] + ["p1",1,"op","chess_char_5_08_a",2,null,515.8,1,6,0,11779,9335,0,131,2], + ["p1",2,"op","chess_char_5_14_a",2,null,0,0,89,24,159150,7595,0,61,6], + ["p1",3,"op","chess_char_5_19_a",0,null,0,0,90,6,38255,15228,0,94,2], + ["p1",4,"op","chess_char_5_07_a",2,null,2564,1,91,2,10656,2984,0,18,1], + ["p1",5,"op","chess_char_5_13_a",2,null,0,0,11,7,58468,14481,0,126,4], + ["p1",6,"op","chess_char_5_17_a",2,null,1154,1,93,3,11205,3361,0,59,0], + ["p1",7,"op","chess_char_5_10_a",2,null,932,1,2,0,8982,0,0,77,2], + ["p1",8,"op","chess_char_5_11_a",2,null,1224,1,3,0,0,0,45325,72,2], + ["p1",9,"op","chess_char_5_12_a",2,null,1160,1,4,1,22227,0,0,86,2], + ["p1",10,"op","chess_char_5_15_a",2,null,0,0,84,2,8190,12762,0,91,2], + ["p1",11,"op","chess_char_5_18_a",2,null,0,0,1,0,5656,1272,0,20,1], + ["p1",12,"op","chess_char_5_20_a",2,null,1972,1,92,1,8370,1972,0,35,1] ], "enemies": { "enemy_1006_shield_2": [1,1,0,679,8000], - "enemy_1006_shield_3": [1,0,1,11277,13035], + "enemy_1006_shield_3": [1,0,1,12959,19581], "enemy_10073_mpcar": [1,1,0,12,1600], "enemy_1007_slime": [1,1,0,0,440], "enemy_10081_mpplai": [1,1,0,0,2000], "enemy_10087_hlchgr": [1,1,0,58,4800], "enemy_10094_crstf": [1,1,0,28,3200], - "enemy_10097_crshd": [1,0,1,936,5046], - "enemy_1166_dusbr_2": [42,27,15,3496,107410], - "enemy_1170_dushld_2": [12,2,8,27941,44857], - "enemy_9011_acrefr": [12,2,4,28337,141112] + "enemy_10097_crshd": [1,1,0,0,8000], + "enemy_1166_dusbr_2": [42,31,11,3354,115737], + "enemy_1170_dushld_2": [12,6,4,28840,54328], + "enemy_9011_acrefr": [12,2,3,23060,125251] }, - "hooks": [1,91,5700,1118,1118,1648,1663,132,49,78,25,27,0,27793,3468,54,74,29,1,49,0,1,9], - "events": {"atk":1244,"bounty":4,"deploy":17,"die":49,"dmg":1661,"fx":130,"heal":126,"leak":29,"skill":50,"spawn":86,"status":661}, - "fx": {"aoe":30,"burst":1,"displace":1,"dodge":16,"dp":22,"phase":1,"slash":48,"sp":1,"spGain":10}, - "snaps": "dbcb5cec 56a8a8f6 e8d97a5d d2594a66 b6f753a4 6b5239d5 0d94f1a8 53bcdab2 582202c7 d547ef42 438122c0 f5345cd2 dbac677c 38aa1103 537a6dbf c9e9e9aa 7c7ed647 9cfd6d5b 94abfb24" + "hooks": [1,93,5700,1154,1154,1775,1770,135,58,77,25,27,0,28761,3090,58,74,19,1,58,0,0,8], + "events": {"atk":1247,"bounty":4,"deploy":19,"die":58,"dmg":1767,"fx":170,"heal":124,"leak":19,"skill":50,"spawn":86,"status":614}, + "fx": {"aoe":60,"displace":1,"dodge":18,"dp":30,"phase":1,"slash":48,"sp":2,"spGain":10}, + "snaps": "dbcb5cec 56a8a8f6 e8d97a5d d2594a66 b6f753a4 78a2f35e 80349f15 2cd292dd d7240cae 4f120b33 69d90058 45d5da9a ae0b244b 17b17251 4df0cf30 f2c7c569 baa94919 07ba89e9 e4a68e4d" }, "standin-04": { "about": "normal band 6 as stand-ins: chess_char_6_03_a→char_610_acfend chess_char_6_02_a→char_608_acpion chess_char_6_07_a→char_617_sharp2 chess_char_6_10_a→char_617_sharp2 chess_char_6_11_a→char_608_acpion chess_char_6_06_a→char_609_acguad chess_char_5_22_a→char_611_acnipe chess_char_6_01_a→char_611_acnipe chess_char_6_04_a→char_614_acsupo chess_char_6_05_a→char_611_acnipe chess_char_6_08_a→char_612_accast chess_char_6_09_a→char_613_acmedc", @@ -241,7 +241,7 @@ "hooks": [1,181,6051,918,918,1945,1944,62,148,170,25,24,0,31170,4160,44,158,22,1,149,2,0,13], "events": {"atk":1080,"bounty":4,"deploy":21,"die":148,"dmg":1944,"fx":87,"heal":45,"layer":2,"leak":22,"skill":49,"spawn":172,"status":440}, "fx": {"aoe":7,"dodge":13,"dp":18,"garrison":2,"revive":1,"slash":29,"sp":12,"spGain":3,"telegraph":2}, - "snaps": "29a29463 6fc96b2d 184b448a fce3c126 45d67357 2b2b80de f966fc58 66e1b5c9 ce0eff29 79805bf9 289d6433 dbecb111 d3142708 bec781dd 26e4cc8e 0c071920 829264cd 362b2f26 16b5df6a 9fda7720 7730ec59" + "snaps": "29a29463 6fc96b2d 184b448a fce3c126 45d67357 4c47175a 1292297b 3e42d44e b7bf91a6 dd4e2386 0b4615ac 81792172 a056e50b 7e87af96 dd9818ad 8c9001ef b2db6556 9bf17d05 66cf3ead 709fbe1f c88b2f46" }, "standin-05": { "about": "normal band 6 as stand-ins: chess_char_6_17_a→char_608_acpion chess_char_6_13_a→char_615_acspec chess_char_6_19_a→char_615_acspec chess_char_6_12_a→char_612_accast chess_char_6_15_a→char_614_acsupo chess_char_6_18_a→char_612_accast chess_char_6_20_a→char_613_acmedc", @@ -426,47 +426,47 @@ "time": 230, "ticks": 6900, "reason": "timeout", - "killed": 32, + "killed": 43, "total": 179, "unspawned": 0, "bossHpLeft": null, - "rngDraws": 1052, + "rngDraws": 1025, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":32,"total":179,"perfect":false,"leaks":147,"leaksUncounted":0,"coins":2,"dmg":1703220,"bossDmg":0,"heal":106481,"deaths":9,"layerGains":{}}}, + "players": {"p1":{"killed":43,"total":179,"perfect":false,"leaks":136,"leaksUncounted":0,"coins":2,"dmg":2172511,"bossDmg":0,"heal":95038,"deaths":13,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_acfend",3285.08,1,201,4,80453,15794,0,116,3], - ["p1",2,"op","chess_char_5_14_b",2,"uniequip_002_acpion",1904,1,200,8,678474,11374,0,31,9], - ["p1",3,"op","chess_char_5_19_b",0,"uniequip_002_sharp2",4154.5,1,202,4,187598,17962,0,51,2], - ["p1",4,"op","chess_char_5_07_b",2,"none",3334,1,199,2,46104,18401,0,36,3], - ["p1",5,"op","chess_char_5_13_b",2,"uniequip_002_acguad",0,0,160,5,101185,25518,0,50,3], - ["p1",6,"op","chess_char_5_17_b",2,"uniequip_002_acspec",1448,1,8,2,21853,46495,0,206,0], - ["p1",7,"op","chess_char_5_10_b",2,"none",1131,1,4,1,46614,0,0,155,4], - ["p1",8,"op","chess_char_5_11_b",2,"uniequip_002_acmedc",2262.4,1,5,0,0,0,106481,74,3], - ["p1",9,"op","chess_char_5_12_b",2,"uniequip_002_acsupo",1430,1,11,2,97118,0,0,130,3], - ["p1",10,"op","chess_char_5_15_b",2,"none",1510,1,13,0,84181,0,0,228,4], - ["p1",11,"op","chess_char_5_18_b",2,"uniequip_002_acnipe",2380.5,1,6,4,265046,0,0,156,3], - ["p1",12,"op","chess_char_5_20_b",2,"uniequip_002_acsupo",1430,1,14,0,94594,0,0,125,3], + ["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_acfend",3371,1,203,4,94861,14912,0,116,3], + ["p1",2,"op","chess_char_5_14_b",2,"uniequip_002_acpion",0,0,124,14,988772,21777,0,61,10], + ["p1",3,"op","chess_char_5_19_b",0,"uniequip_002_sharp2",4154.5,1,205,8,235963,23398,0,64,2], + ["p1",4,"op","chess_char_5_07_b",2,"none",3334,1,204,2,34977,19978,0,29,2], + ["p1",5,"op","chess_char_5_13_b",2,"uniequip_002_acguad",0,0,161,5,103856,25240,0,51,3], + ["p1",6,"op","chess_char_5_17_b",2,"uniequip_002_acspec",1448,1,200,4,83107,24504,0,112,0], + ["p1",7,"op","chess_char_5_10_b",2,"none",1131,1,4,2,79514,0,0,153,4], + ["p1",8,"op","chess_char_5_11_b",2,"uniequip_002_acmedc",2262.4,1,5,0,0,0,95038,74,3], + ["p1",9,"op","chess_char_5_12_b",2,"uniequip_002_acsupo",1430,1,11,0,112393,0,0,146,3], + ["p1",10,"op","chess_char_5_15_b",2,"none",1510,1,13,0,77843,0,0,215,4], + ["p1",11,"op","chess_char_5_18_b",2,"uniequip_002_acnipe",2380.5,1,6,4,262886,0,0,169,3], + ["p1",12,"op","chess_char_5_20_b",2,"uniequip_002_acsupo",1430,1,14,0,98338,0,0,125,3], ["-","~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], ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null] ], "enemies": { "enemy_1072_dlancer": [1,1,0,3008,37150], - "enemy_1081_sotisd": [1,1,0,39568,92876], - "enemy_1092_mdgint": [1,0,1,0,67], + "enemy_1081_sotisd": [1,0,1,0,27122], + "enemy_1092_mdgint": [1,0,1,1448,67], "enemy_1116_liprr": [1,0,1,0,7702], "enemy_1116_liprr_2": [1,0,1,0,1829], "enemy_1118_lidbox_2": [1,0,1,0,1344], - "enemy_1119_vofsd": [1,0,1,0,959], - "enemy_1121_lifbos": [1,0,1,0,922], - "enemy_1249_lysdb_2": [60,9,49,23770,693077], - "enemy_1251_lysyta": [63,20,43,6676,488381], - "enemy_1254_lypa_2": [48,1,47,62523,378913] + "enemy_1119_vofsd": [1,0,1,0,889], + "enemy_1121_lifbos": [1,0,1,0,1525], + "enemy_1249_lysdb_2": [60,13,44,41971,986578], + "enemy_1251_lysyta": [63,28,35,8822,665956], + "enemy_1254_lypa_2": [48,1,47,74560,442348] }, - "hooks": [1,202,6900,1760,1760,3076,3094,145,41,186,40,39,0,26130,16747,54,179,145,1,45,0,0,8], - "events": {"atk":1979,"bounty":1,"deploy":20,"die":41,"dmg":3094,"fx":274,"heal":126,"leak":145,"skill":79,"spawn":194,"status":1237}, - "fx": {"aoe":84,"dodge":29,"dp":17,"shieldBreak":60,"slash":72,"sp":5,"spGain":7}, - "snaps": "4d4dc5ab 98e9e455 46d76035 722418a9 b67e68a3 866e42e8 786ec7e2 2c2a7307 3657963b 8fad1c0c 513f1b73 84a84fa5 d1698d65 9ae19dde 551dcc8c 8e2b8aef a8f4f525 70f7b5a7 f8b9e9b9 0c6627fe 9c4013d8 fa4c2add 6f3c1f53" + "hooks": [1,205,6900,1764,1764,3493,3533,147,56,189,40,44,0,26478,16569,72,179,133,1,59,0,0,10], + "events": {"atk":2013,"bounty":1,"deploy":23,"die":56,"dmg":3508,"fx":308,"heal":130,"leak":133,"skill":80,"spawn":194,"status":1297}, + "fx": {"aoe":90,"displace":12,"dodge":31,"dp":24,"shieldBreak":59,"slash":80,"sp":1,"spGain":11}, + "snaps": "4d4dc5ab 00efc642 4dc3aa32 13e9533b a3617bdf 587927d0 9fe3b7b8 0b39dceb 2981edf9 2ba1ce8c ef825d5b 85fd9608 a83f0a12 9e6b1271 7c5bf7ee f6ad8d8e c76286f6 037365de bf979497 771a38f7 b74aaf11 518326a3 19e5e338" }, "standin-09": { "about": "elite band 7 as stand-ins: chess_char_6_03_b→char_610_acfend chess_char_6_02_b→char_608_acpion chess_char_6_07_b→char_617_sharp2 chess_char_6_10_b→char_617_sharp2 chess_char_6_11_b→char_608_acpion chess_char_6_06_b→char_609_acguad chess_char_5_22_b→char_611_acnipe chess_char_6_01_b→char_611_acnipe chess_char_6_04_b→char_614_acsupo chess_char_6_05_b→char_611_acnipe chess_char_6_08_b→char_612_accast chess_char_6_09_b→char_613_acmedc", @@ -477,31 +477,31 @@ "round": 13, "seed": 2308517178, "waveId": "act1autochess_h06", - "time": 199.53, - "ticks": 5986, + "time": 198.9, + "ticks": 5967, "reason": "cleared", "killed": 158, "total": 158, "unspawned": 0, "bossHpLeft": null, - "rngDraws": 279, + "rngDraws": 257, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":158,"total":158,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":8,"dmg":924432,"bossDmg":0,"heal":29522,"deaths":0,"layerGains":{"sargonShip":16}} + "p1": {"killed":158,"total":158,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":8,"dmg":924432,"bossDmg":0,"heal":30129,"deaths":1,"layerGains":{"sargonShip":20}} }, "units": [ ["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_acfend",4974.95,1,8,0,0,0,0,0,0], ["p1",2,"op","chess_char_6_02_b",2,"uniequip_002_acpion",1904,1,6,0,0,0,0,0,0], ["p1",3,"op","chess_char_6_07_b",0,"uniequip_002_sharp2",2494,1,9,0,0,0,0,0,0], - ["p1",4,"op","chess_char_6_10_b",0,"uniequip_002_sharp2",2494,1,11,24,119442,0,0,173,3], - ["p1",5,"op","chess_char_6_11_b",2,"uniequip_002_acpion",1624,1,13,38,184185,11513,0,52,20], - ["p1",6,"op","chess_char_6_06_b",2,"uniequip_002_acguad",3396,1,14,19,66075,855,0,156,4], + ["p1",4,"op","chess_char_6_10_b",0,"uniequip_002_sharp2",2494,1,11,20,146204,0,0,174,4], + ["p1",5,"op","chess_char_6_11_b",2,"uniequip_002_acpion",1834,1,13,45,168227,12564,0,58,23], + ["p1",6,"op","chess_char_6_06_b",2,"uniequip_002_acguad",3531,1,14,10,62057,943,0,157,4], ["p1",7,"op","chess_char_5_22_b",2,"uniequip_002_acnipe",1587,1,4,0,0,0,0,0,0], ["p1",8,"op","chess_char_6_01_b",2,"uniequip_002_acnipe",1643,1,5,0,0,0,0,0,0], - ["p1",9,"op","chess_char_6_04_b",2,"uniequip_002_acsupo",83.4,1,15,11,95944,2382,0,135,2], - ["p1",10,"op","chess_char_6_05_b",2,"uniequip_002_acnipe",1643,1,7,19,77978,0,0,93,2], - ["p1",11,"op","chess_char_6_08_b",2,"uniequip_002_accast",1819,1,10,47,380809,0,0,82,2], - ["p1",12,"op","chess_char_6_09_b",2,"uniequip_002_acmedc",1324,1,12,0,0,17709,29522,83,2], + ["p1",9,"op","chess_char_6_04_b",2,"uniequip_002_acsupo",2465,1,137,8,65378,2465,0,91,2], + ["p1",10,"op","chess_char_6_05_b",2,"uniequip_002_acnipe",1643,1,7,22,90616,0,0,96,2], + ["p1",11,"op","chess_char_6_08_b",2,"uniequip_002_accast",1819,1,10,53,391951,0,0,83,2], + ["p1",12,"op","chess_char_6_09_b",2,"uniequip_002_acmedc",1394,1,12,0,0,16762,30129,83,3], ["-","~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], ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null] @@ -512,17 +512,17 @@ "enemy_1148_dssbr_2": [1,1,0,0,3456], "enemy_1158_divman": [1,1,0,195,3168], "enemy_1160_hvyslr": [1,1,0,0,9600], - "enemy_1160_hvyslr_2": [1,1,0,0,13440], - "enemy_1161_tidmag": [1,1,0,1363,5760], - "enemy_1161_tidmag_2": [1,1,0,824,7680], - "enemy_1249_lysdb_2": [60,60,0,1374,345600], + "enemy_1160_hvyslr_2": [1,1,0,133,13440], + "enemy_1161_tidmag": [1,1,0,1336,5760], + "enemy_1161_tidmag_2": [1,1,0,1391,7680], + "enemy_1249_lysdb_2": [60,60,0,1459,345600], "enemy_1251_lysyta": [51,51,0,0,134640], - "enemy_1329_cbshld": [39,39,0,3643,374400] + "enemy_1329_cbshld": [39,39,0,3019,374400] }, - "hooks": [1,173,5986,867,867,1620,1744,149,158,158,35,34,0,31554,3298,63,158,0,1,158,2,1,21], - "events": {"atk":965,"bounty":4,"deploy":12,"die":158,"dmg":1722,"fx":309,"heal":116,"layer":2,"skill":69,"spawn":173,"status":425}, - "fx": {"aoe":2,"burst":1,"dodge":27,"dp":58,"garrison":2,"shieldBreak":60,"slash":146,"sp":13}, - "snaps": "268546d4 ec010f3d d556f759 749d85b5 fd1848fa 9a69b0ed 35a96974 ceeb72dd adfeed08 f497b058 ffade80c ec17b5c7 49c46293 8eebc960 34dab525 36f0055b b3c7d1bd 036e5819 41df2c8f 33def1f4" + "hooks": [1,174,5967,836,836,1660,1803,179,159,159,40,38,0,28209,2688,60,158,0,1,159,3,1,23], + "events": {"atk":974,"bounty":4,"deploy":13,"die":159,"dmg":1782,"fx":330,"heal":135,"layer":3,"skill":78,"spawn":173,"status":334}, + "fx": {"aoe":2,"burst":1,"dodge":28,"dp":68,"garrison":3,"shieldBreak":60,"slash":152,"sp":16}, + "snaps": "526d4c63 f04a2d68 53d511ef 5f8b4334 d796abdd 122e9876 81bad943 4c6a838a eb463850 1e1d2025 875b599b 2d763f55 51548a07 73b07326 88a8ea6f ec211cd8 e329b226 03d1aa7b 63fd2bb2 afee939a" }, "standin-10": { "about": "elite band 7 as stand-ins: chess_char_6_17_b→char_608_acpion chess_char_6_13_b→char_615_acspec chess_char_6_19_b→char_615_acspec chess_char_6_12_b→char_612_accast chess_char_6_15_b→char_614_acsupo chess_char_6_18_b→char_612_accast chess_char_6_20_b→char_613_acmedc", @@ -569,10 +569,10 @@ "enemy_1169_duphlx": [1,0,1,0,0], "enemy_1402_tgshd_2": [36,2,34,29224,197345] }, - "hooks": [1,165,6486,549,549,865,877,6809,50,159,17,36,0,8486,1125,49,131,106,1,50,1,1,0], + "hooks": [1,165,6486,549,549,865,877,6809,50,159,17,36,0,8308,1125,49,131,106,1,50,1,1,0], "events": {"atk":554,"deploy":32,"die":53,"dmg":802,"fx":530,"heal":21,"layer":1,"leak":106,"skill":34,"spawn":140,"status":378}, "fx": {"aoe":4,"blink":3,"burst":1,"displace":17,"dodge":3,"dp":17,"expose":414,"garrison":1,"garrisonGrant":3,"slash":67}, - "snaps": "80709a69 1b180600 2040e410 16970933 0b0b610d 4e3060e6 d2f5fa1d d015c650 26f1c266 9350b3c3 ad710861 7c181d0b d41c00a0 64db1e01 858cfe5f 40faa038 5cf23687 498f5f47 50bc9ad2 848c94df 4806ebb3 9bfdaf83" + "snaps": "80709a69 1b180600 2040e410 16970933 7ec28fab 061d6583 09f2da3e 98333fee 741e4051 c5f8165a 55db9474 b9b2bf46 d72aea5d 20d591d8 3077f062 ab32900d eb4540a2 37c492a6 3e3cd8f7 20a4cb8e 22ada53e b9ce62f2" } } }