diff --git a/server/sim/content/bands/battle.js b/server/sim/content/bands/battle.js index 67e8d30d..a1fc8d91 100644 --- a/server/sim/content/bands/battle.js +++ b/server/sim/content/bands/battle.js @@ -7,7 +7,8 @@ // act1autochess_band2_buff 阿米娅 众志合一 ≥ value_i active bonds (highest i) ⇒ every operator ATK +atk_i, // HP +max_hp_i (直接乘算 — support directMods —, bonds snapshot at combat start) // act1autochess_band28_buff 埃芒加德 命结之秘 the first max_respawn_cnt operator knock-downs of the battle revive at -// once at full HP (runs after every other death saver, items included) +// once at full HP (runs after every other death saver, items included; +// a new deployment for 坚固维式重锤's lock — items revivedInPlace) // act1autochess_band13_buff 克莱门莎 崇高牺牲 a operator knocked down ⇒ +its tier (等阶) layers // (bond_add_type by_charlevel; no "已激活" in the text ⇒ requireActive false) // act1autochess_band16_buff 大帝 加急调派 "每次部署后再部署时间减少50%": every deployment of an operator stacks one @@ -36,7 +37,7 @@ import { num, buffsOf, bandRecord, isOp, onField, isElite, tierOf, unitBonds, activeBondIds, playerOps, passiveBuff, fxOn, matchBands, gainLayers, alliesAround, N4, baseChessId, isGroundOp, directMods, } from '../support/index.js'; -import { weaknessRetype, addShieldLayer, PRIO_REVIVE } from '../items/battle.js'; +import { weaknessRetype, addShieldLayer, PRIO_REVIVE, revivedInPlace } from '../items/battle.js'; import { spawnMapChar } from '../tokens.js'; export const AMEDIC_BAND = 'band_amedic'; @@ -73,6 +74,7 @@ const BY_KEY = { used++; c.prevented = true; u.hp = u.s.maxHp; + revivedInPlace(u); // in place for PRTS's 0-time / 0-cost redeploy: a new deployment for 坚固维式重锤's lock (items) fxOn(battle, 'revive', u, keyOf(bandId), bandId, { left: max - used }); }, { priority: PRIO_BAND_REVIVE }); }, diff --git a/server/sim/content/items/battle.js b/server/sim/content/items/battle.js index 88fdb81f..96c1cbfe 100644 --- a/server/sim/content/items/battle.js +++ b/server/sim/content/items/battle.js @@ -20,11 +20,13 @@ // either quality satisfies them; lent items count as carried. Both are evaluated live, at the moment of use. // // Hook priorities (non-default): 'fatal' — consumable death savers run LAST, so a skill's / talent's own undying (kits -// use 10 … −60) never wastes a charge: 坚固维式重锤's lock (异常效果 不死, once per deployment) at PRIO_REVIVE −100, then the -// M3茧甲 revive at PRIO_RESPAWN −101 — PRTS 卫戍协议:盟约 下半/PRTS盟约记录 备注 "“复活”的实现方式为:受益者因移动之外的 -// 原因退场时下次部署的再部署时间和费用归零": a revive acts on a knock-out, which a 不死 prevents, so the lock always comes -// first whatever the equip order (player report F1 after 0.1.0: with the 茧甲 equipped first the revive ran first and -// the first lethal hit showed no lock); 埃芒加德's band revive follows (bands/battle.js PRIO_BAND_REVIVE −110). +// use 10 … −60) never wastes a charge: 坚固维式重锤's lock (异常效果 不死, once per deployment — deploymentOf; its running +// windows held by one battle-level hook) at PRIO_REVIVE −100, then the M3茧甲 revive at PRIO_RESPAWN −101 — PRTS +// 卫戍协议:盟约 下半/PRTS盟约记录 备注 "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的再部署时间和费用归零": a +// revive acts on a knock-out, which a 不死 prevents, so the lock always comes first whatever the equip order (player +// report F1 after 0.1.0: with the 茧甲 equipped first the revive ran first and the first lethal hit showed no lock); +// 埃芒加德's band revive follows (bands/battle.js PRIO_BAND_REVIVE −110). Both revives stand in place for that redeploy, +// so each opens a new deployment for the lock (revivedInPlace). // 骑士戒律's free in-skill undying runs early (20). Flat damage reduction 'hit' −10 (after the other damage modifiers). // Proc damage dealt by items carries the tag 'item' and never re-triggers item procs. // @@ -219,11 +221,38 @@ function fieldHammers(battle, rt, pid) { function hammerState(battle, rt, u) { let hs = rt.hammers.get(u); if (!hs) { - hs = { own: { burn: 0, undying: 0, aspd: 0, tremble: 0 }, params: {}, steam: 0, steamP: null, refs: 0, scope: null, aspdCur: 0, undyingUsed: false, undyingUntil: -Infinity }; + // lockAt: the deployment (deploymentOf) whose 坚固 lock is spent — kept here, not in the grant's Scope, so a lend + // that ends and comes back never resets it, and a redeploy while no hammer is held still re-arms it + hs = { own: { burn: 0, undying: 0, aspd: 0, tremble: 0 }, params: {}, steam: 0, steamP: null, refs: 0, scope: null, aspdCur: 0, lockAt: null }; rt.hammers.set(u, hs); } return hs; } + +// 坚固维式重锤's 不死 lock — once per DEPLOYMENT (the user's first-hand memory of the official mode, 2026-10-03: "每次部署 +// 一次"; the text only says 首次). The lock belongs to the deployment, not to the grant that gave it: a borrower (萨尔贡 × +// 娜仁图亚's 60 s lend) follows the same rule as an owner. + +/** + * The deployment `u` is in: every deploy bumps `deploySeq` (the redeploy after a knock-out, a 突袭 retreat + redeploy + * [ASSUMED a deployment], 阿戈尔's 立刻复活), and an in-place 复活 (M3茧甲, 埃芒加德: revivedInPlace) opens a new one too + * [ASSUMED] — PRTS (M3茧甲 / 埃芒加德 / 阿戈尔 备注) "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的再部署时间和 + * 费用归零": officially a revive is a 0-time / 0-cost redeploy; the remake keeps the unit standing instead. + */ +function deploymentOf(u) { return `${u.deploySeq}:${u.mem.revives | 0}`; } + +/** An in-place 复活 (M3茧甲, 埃芒加德) happened: a new deployment for the once-per-deployment lock (deploymentOf). */ +export function revivedInPlace(u) { if (u && u.mem) u.mem.revives = (u.mem.revives | 0) + 1; } + +/** + * Does `u` hold 坚固维式重锤's 不死 right now — a window started in this deployment that has not run out? The window lives + * on the unit (`mem.undyingUntil`, `mem.undyingAt`) and a battle-level hook holds it (hammerAcquire), so it outlives the + * grant that started it — a lend running out mid-window leaves the 不死 for its 8 s [ASSUMED: the 异常效果 outlasts its + * source] — and ends with the deployment. 信仰搅拌机 S2 steps aside while it holds (kits/tier4.js). + */ +export function holdsUndying(battle, u) { + return !!u && u.mem.undyingAt === deploymentOf(u) && battle.time < (u.mem.undyingUntil ?? -Infinity); +} /** Effective multiplier of a hammer type on `u` and its params (null when the type does not apply). */ function hammerMul(battle, rt, u, hs, type) { const own = hs.own[type]; @@ -247,6 +276,10 @@ function hammerAcquire(battle, rt, u) { const hs = hammerState(battle, rt, u); hs.refs++; if (hs.scope) return hs; + // every running 坚固 window of the battle (holdsUndying): one hook that no grant owns, registered before any lock hook + if (!rt.undyingHook) { + rt.undyingHook = battle.on('fatal', (c) => { if (!c.prevented && holdsUndying(battle, c.unit)) c.prevented = true; }, { priority: PRIO_REVIVE }); + } const S = new Scope(battle, u, `item:hammer#${u.id}`); hs.scope = S; hs.aspdCur = 0; @@ -272,29 +305,22 @@ function hammerAcquire(battle, rt, u) { if (t && t.side === 'enemy' && t.alive && chance(battle, pr)) battle.applyStatus(t, 'tremble', { duration: num(p.disarmed_duration, 2), source: u }); } }); - // 坚固: first lethal hit of each deployment ⇒ HP never below 1 for undeadable_duration × m s. Once per DEPLOYMENT (the - // user's first-hand memory of the official mode, 2026-10-03: "每次部署一次"; the text only says 首次): the carrier's own - // non-initial 'deploy' — the redeploy after a knock-out, a 突袭 jump (retreat + redeploy; [ASSUMED] a deployment too) - // — re-arms the lock and ends a window still running (the retreat ends the unit's states). Any lethal HP loss sets - // it off, an ally's (the 阿戈尔 battle-start devour, "造成5000点物理伤害") or the carrier's own (源石溶剂) included. - S.on('deploy', (c) => { - if (c.unit !== u || c.initial) return; - hs.undyingUsed = false; - hs.undyingUntil = -Infinity; - if (u.mem.undyingUntil != null) u.mem.undyingUntil = -Infinity; - }, 100); + // 坚固: the first lethal hit of each deployment (deploymentOf) ⇒ HP never below 1 for undeadable_duration × m s — the + // window, held by the battle-level hook above. A new deployment re-arms the lock and leaves a window still running + // behind (the retreat ends the unit's states). Any lethal HP loss sets it off, an ally's (the 阿戈尔 battle-start + // devour, "造成5000点物理伤害") or the carrier's own (源石溶剂) included. S.on('fatal', (c) => { if (c.unit !== u || c.prevented) return; - if (battle.time < hs.undyingUntil) { c.prevented = true; return; } - if (hs.undyingUsed) return; + const at = deploymentOf(u); + if (hs.lockAt === at) return; const p = hammerParams(hs, 'undying'); if (!p) return; const m = hammerMul(battle, rt, u, hs, 'undying'); if (!(m > 0)) return; - hs.undyingUsed = true; + hs.lockAt = at; const dur = num(p.undeadable_duration, 8) * m; - hs.undyingUntil = battle.time + dur; - u.mem.undyingUntil = hs.undyingUntil; // the carrier holds 不死 (信仰搅拌机 S2 steps aside: kits/tier4.js) + u.mem.undyingUntil = battle.time + dur; // the carrier holds 不死 (holdsUndying) + u.mem.undyingAt = at; c.prevented = true; fxOn(battle, 'undying', u, 'item:hammer', 'chess_item_3_09_e', { duration: dur }); }, PRIO_REVIVE); @@ -565,7 +591,8 @@ const BY_ITEM = { if (addShieldLayer(battle, c.target, SHIELD_KEY, cap)) fxOn(battle, 'shield', c.target, 'item:chess_item_4_11_e', rec.id); }); }, - // M3茧甲: knocked down in battle ⇒ revive at full HP (max_respawn_cnt per battle) + // M3茧甲: knocked down in battle ⇒ revive at full HP (max_respawn_cnt per battle), in place — PRTS's form (退场, then a + // 0-time / 0-cost redeploy) is not modelled, but it counts as a new deployment for 坚固维式重锤's lock (revivedInPlace) chess_item_4_12_e(battle, u, rec, S) { const p = bp(rec, 'act1autochess_equip_acarm068_global_buff'); const max = p ? Math.floor(num(p.max_respawn_cnt, 1)) : 0; @@ -576,6 +603,7 @@ const BY_ITEM = { used++; c.prevented = true; u.hp = u.s.maxHp; + revivedInPlace(u); fxOn(battle, 'revive', u, 'item:chess_item_4_12_e', rec.id, { left: max - used }); }, PRIO_RESPAWN); // after the hammer's 不死 lock, whatever the equip order (header) }, diff --git a/server/sim/content/kits/tier4.js b/server/sim/content/kits/tier4.js index f34aec69..944b7e3d 100644 --- a/server/sim/content/kits/tier4.js +++ b/server/sim/content/kits/tier4.js @@ -33,6 +33,7 @@ import { normalizeChess } from '../../simdata.js'; import { aggregateMods } from '../../buffs.js'; import { CAT_SHIELD_KEY } from '../tokens.js'; import { isHpLoss } from '../../damage.js'; +import { holdsUndying } from '../items/battle.js'; const TICK_EPS = 0.01; // minimal status duration (s) const AURA = 0.2; // aura refresh period (s) @@ -287,13 +288,13 @@ const kits = { // before the hit) and N bullets go; with fewer left it still blocks, spends them all and the skill ends (PRTS 备注 // "弹药量不足时仍可抵挡致命伤害,此时将消耗所有剩余弹药并退出技能状态"). "自身持有不死时此效果不会生效": a 不死 // that runs before it (骑士戒律, priority 20) has already prevented the hit; 坚固维式重锤's held window - // (unit.mem.undyingUntil, items/battle.js) runs last, so the guard steps aside while it lasts + // (items/battle.js holdsUndying: this deployment's, not run out) runs last, so the guard steps aside while it lasts const cost = Math.max(1, Math.floor(num(bb.ammo_cost, 30))); battle.on('hit', (c) => { if (c.target === unit) unit.mem.rmixerPre = { dmg: c.dmg, hp: unit.hp }; }, { owner: unit, priority: -100 }); battle.on('fatal', (c) => { const sk = unit.skill; if (c.unit !== unit || c.prevented || !sk || !sk.active || sk.kind !== 'ammo' || !(sk.ammoLeft > 0)) return; - if (battle.time < (unit.mem.undyingUntil ?? -Infinity)) return; + if (holdsUndying(battle, unit)) return; c.prevented = true; const pre = unit.mem.rmixerPre; unit.hp = pre && pre.dmg === c.dmg ? Math.max(1, Math.min(unit.s.maxHp, pre.hp)) : 1; diff --git a/test/content/feedback1f-hammer.test.js b/test/content/feedback1f-hammer.test.js index ff1c761a..b99b52d1 100644 --- a/test/content/feedback1f-hammer.test.js +++ b/test/content/feedback1f-hammer.test.js @@ -6,12 +6,14 @@ // lethal hit showed no lock. PRTS (same page, M3茧甲 / 埃芒加德 / 阿戈尔 备注): "“复活”的实现方式为:受益者因移动之外的原因退场时 // 下次部署的再部署时间和费用归零" — a revive acts on a knock-out (退场), which a 不死 prevents, so the lock always comes first // (items/battle.js PRIO_RESPAWN). The scope is one lock per DEPLOYMENT (the user's decision, 2026-10-03: "每次部署一次"): -// until then a redeployed carrier had none, the likeliest reading of the report. Pinned too: the 阿戈尔 battle-start -// devour spends the lock of the first deployment. +// until then a redeployed carrier had none, the likeliest reading of the report. The lock belongs to the deployment, +// not to the grant (QA after the integration: a hammer lent by 萨尔贡 × 娜仁图亚 came back spent, and its window ended +// with the lend), and an in-place 复活 (M3茧甲, 埃芒加德) is a new deployment — PRTS's 复活 is a 0-time / 0-cost +// redeploy [ASSUMED]. Pinned too: the 阿戈尔 battle-start devour spends the lock of the first deployment. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { makeBattle, chessRec, enemyRec } from '../helpers/battleHarness.js'; -import { PRIO_REVIVE, PRIO_RESPAWN } from '../../server/sim/content/items/battle.js'; +import { PRIO_REVIVE, PRIO_RESPAWN, holdsUndying, lendItemEffects, itemGrants } from '../../server/sim/content/items/battle.js'; import { PRIO_BAND_REVIVE } from '../../server/sim/content/bands/battle.js'; const HAMMER = 'chess_item_3_09_e_a'; @@ -117,11 +119,11 @@ test('F1 坚固维式重锤: a retreat + redeploy (a 突袭 jump) is a new deplo const c = carrier([HAMMER]); const hit = () => c.h.b.dealDamage(c.e, c.u, { amount: 1e7, type: 'true', canDodge: false }); hit(); - assert.ok(c.u.alive && c.u.mem.undyingUntil > c.h.b.time, 'lock running'); + assert.ok(c.u.alive && holdsUndying(c.h.b, c.u), 'lock running'); c.h.run(2); c.h.b.retreat(c.u, { reason: 'raid' }); assert.ok(c.h.b.redeploy(c.u, { free: true, tile: [10, 5], keepSp: true }), 'redeployed one tile on'); - assert.ok(!(c.u.mem.undyingUntil > c.h.b.time), 'the first deployment\'s window ended with it'); + assert.ok(!holdsUndying(c.h.b, c.u), 'the first deployment\'s window ended with it'); c.h.run(1); hit(); assert.ok(c.u.alive && c.u.hp >= 1 && c.u.hp < 2, 'the new deployment has its own lock'); @@ -167,8 +169,102 @@ test('F1 坚固维式重锤 + M3茧甲: the lock comes before the revive whateve assert.ok(c.u.alive, `${items}: after the 8 s the 茧甲 revives`); assert.equal(Math.round(c.u.hp), 2000, 'full HP'); assert.deepEqual(log.map((x) => x[0]), ['undying', 'revive']); + // the revive stands for PRTS's 0-time / 0-cost redeploy: a new deployment, so the lock is armed again hit(); - assert.equal(c.u.alive, false, `${items}: nothing left`); + assert.ok(c.u.alive && c.u.hp < 2, `${items}: the revived carrier locks again`); + assert.deepEqual(log.map((x) => x[0]), ['undying', 'revive', 'undying']); + c.h.run(8.2); + hit(); + assert.equal(c.u.alive, false, `${items}: nothing left (one revive, one lock in that deployment)`); + } +}); + +// The 埃芒加德 strategy's revive (命结之秘, the first 3 knock-downs of the battle revive at once) stands in place for the same +// redeploy as M3茧甲's: the lock is armed again after it. +test('F1 坚固维式重锤 + 埃芒加德: the band revive re-arms the lock, once per revive', () => { + const h = makeBattle({ + defs: { chess: { t_op: op('t_op') }, enemies: { e_d: enemyRec({ key: 'e_d', hp: 1e7, speed: 0 }) } }, + units: [{ chessId: 't_op', row: 10, col: 4, items: [HAMMER] }], bandId: 'band_ermengard', + enemies: [{ key: 'e_d', pos: [10, 9] }], timeLimit: 999, autoFinish: false, + }); + h.step(1); + const u = h.unit('t_op'); + const e = h.b.enemies.find((x) => x.alive); + const log = fxLog(h, u); + const hit = () => h.b.dealDamage(e, u, { amount: 1e7, type: 'true', canDodge: false }); + hit(); + h.run(8.2); + hit(); + assert.ok(u.alive && Math.round(u.hp) === 2000, 'the band revives the carrier after its first lock'); + hit(); + assert.ok(u.alive && u.hp < 2, 'locked again after the revive'); + h.run(1); + hit(); + assert.ok(u.alive && u.hp < 2, 'inside the new window'); + assert.deepEqual(log.map((x) => [x[0], x[2]]), [['undying', 'item:hammer'], ['revive', 'band:band_ermengard'], ['undying', 'item:hammer']]); +}); + +// 萨尔贡 × 娜仁图亚 lends a carrier's items to its neighbours for 60 s (items/battle.js lendItemEffects). The lock belongs to +// the borrower's deployment, not to the lend: a borrower knocked out while lent, whose lend ran out while it was down, +// gets a fresh lock when it is lent the hammer again in its next deployment (QA: it came back spent — the per-grant +// hook that re-armed it was gone with the lend); and a window started through the lend lasts its 8 s after the lend +// ends [ASSUMED: the 异常效果 不死 outlasts its source] (QA: it ended with the lend). +/** A lender carrying the hammer at (10, 4), a borrower (no items) at (11, 4) and a static dummy. */ +function lendPair() { + const h = makeBattle({ + defs: { chess: { t_op: op('t_op'), t_bor: op('t_bor') }, enemies: { e_d: enemyRec({ key: 'e_d', hp: 1e7, speed: 0 }) } }, + units: [{ chessId: 't_op', row: 10, col: 4, items: [HAMMER] }, { chessId: 't_bor', row: 11, col: 4 }], + enemies: [{ key: 'e_d', pos: [10, 9] }], timeLimit: 999, autoFinish: false, + }); + h.step(1); + return { h, lender: h.unit('t_op'), u: h.unit('t_bor'), e: h.b.enemies.find((x) => x.alive) }; +} +const lent = (h, u) => itemGrants(h.b, u).some((g) => g.lent && g.key === 'chess_item_3_09_e'); + +test('F1 坚固维式重锤 lent by 萨尔贡 × 娜仁图亚: a fresh lock in the next deployment, though the lend ran out while it was down', () => { + { + const { h, lender, u, e } = lendPair(); + const log = fxLog(h, u); + const hit = () => h.b.dealDamage(e, u, { amount: 1e7, type: 'true', canDodge: false }); + assert.equal(lendItemEffects(h.b, lender, u, { duration: 12 }), 1, 'the hammer is lent'); + hit(); + assert.ok(u.alive && holdsUndying(h.b, u), 'the lent hammer locks'); + h.run(8.2); + hit(); + assert.equal(u.alive, false, 'knocked out after the window'); + h.run(5); + assert.ok(!lent(h, u), 'the lend ran out while it was down'); + h.runUntil(() => u.alive, 60); + assert.ok(u.alive, 'redeployed'); + hit(); + assert.equal(u.alive, false, 'no hammer, no lock'); + h.runUntil(() => u.alive, 60); + assert.equal(lendItemEffects(h.b, lender, u, { duration: 12 }), 1, 'lent again in this deployment'); + hit(); + assert.ok(u.alive && u.hp < 2, 'a new deployment with the hammer: a fresh lock'); + h.run(8.2); + lendItemEffects(h.b, lender, u, { duration: 12 }); + hit(); + assert.equal(u.alive, false, 'a lend refreshed in the same deployment does not re-arm it'); + assert.equal(log.filter((x) => x[0] === 'undying').length, 2); + } +}); + +test('F1 坚固维式重锤 lent by 萨尔贡 × 娜仁图亚: a window started through the lend lasts its 8 s after the lend ends', () => { + { + const { h, lender, u, e } = lendPair(); + const hit = () => h.b.dealDamage(e, u, { amount: 1e7, type: 'true', canDodge: false }); + lendItemEffects(h.b, lender, u, { duration: 3 }); + h.run(2); + hit(); + assert.ok(u.alive && u.hp < 2, 'locked 1 s before the lend ends'); + h.run(3); + assert.ok(!lent(h, u), 'the lend is over'); + hit(); + assert.ok(u.alive && holdsUndying(h.b, u), 'still held: the window runs its 8 s'); + h.run(5.2); + hit(); + assert.equal(u.alive, false, 'knocked out once the 8 s are over'); } }); diff --git a/test/sim/feedback1-residuals.test.js b/test/sim/feedback1-residuals.test.js index d90afd3a..d031c94e 100644 --- a/test/sim/feedback1-residuals.test.js +++ b/test/sim/feedback1-residuals.test.js @@ -9,6 +9,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js'; import { getData } from '../../server/data.js'; +import { holdsUndying } from '../../server/sim/content/items/battle.js'; const DATA = getData({ log: { warn() {}, error() {}, info() {} } }); @@ -84,7 +85,7 @@ test('信仰搅拌机 S2 steps aside while she holds 不死 (坚固维式重锤\ const foe = h.b.enemies[0]; const lethal = () => h.b.dealDamage(foe, u, { amount: u.s.maxHp * 10, type: 'true', tags: ['test'] }); lethal(); // before the skill: the hammer's lock starts (8 s of 不死) - assert.ok(u.alive && u.mem.undyingUntil > h.b.time, 'the hammer holds her'); + assert.ok(u.alive && holdsUndying(h.b, u), 'the hammer holds her'); assert.ok(u.skill.activate('test', { free: true }), 'S2 on'); const ammo = u.skill.ammoLeft; lethal(); @@ -97,3 +98,28 @@ test('信仰搅拌机 S2 steps aside while she holds 不死 (坚固维式重锤\ assert.equal(u.skill.ammoLeft, ammo - 30); assert.equal(h.b.errorCount, 0); }); + +test('信仰搅拌机 S2 guards again in a new deployment although the old 不死 window has not run out (holdsUndying is per deployment)', () => { + const id = 'chess_char_4_01_a'; + const h = makeBattle({ + defs: { enemies: { e_hit: enemyRec({ key: 'e_hit', hp: 1e8, atk: 0, speed: 0 }) } }, + units: [{ chessId: id, row: 9, col: 5, skillIndex: 1, items: ['chess_item_3_09_e_a'] }], enemies: [{ key: 'e_hit', pos: [9, 8] }], + timeLimit: 60, autoFinish: false, + }); + h.step(2); + const u = h.unit(id); + const foe = h.b.enemies[0]; + const lethal = () => h.b.dealDamage(foe, u, { amount: u.s.maxHp * 10, type: 'true', tags: ['test'] }); + lethal(); + assert.ok(holdsUndying(h.b, u), 'the hammer\'s window runs'); + h.run(1); + h.b.retreat(u, { reason: 'raid' }); // a 突袭-style retreat + redeploy 3 s into the window + assert.ok(h.b.redeploy(u, { free: true, tile: [9, 6] }), 'redeployed'); + assert.ok(!holdsUndying(h.b, u), 'the window ended with its deployment'); + assert.ok(u.skill.activate('test', { free: true }), 'S2 on'); + const ammo = u.skill.ammoLeft; + lethal(); + assert.ok(u.alive, 'S2 blocks it'); + assert.equal(u.skill.ammoLeft, ammo - 30, 'she holds no 不死 in this deployment: S2 spends its bullets'); + assert.equal(h.b.errorCount, 0); +});