feedback1 integrate: 坚固维式重锤's lock belongs to the deployment - a lent hammer and in-place revives follow it (QA)

The lock and its re-arm hook lived in the carrier's hammer grants, so a hammer lent by 萨尔贡 × 娜仁图亚 came back spent
in the borrower's next deployment once the lend had run out while it was down, and a window started through the lend
ended with it. deploymentOf marks the deployment whose lock is spent; one battle-level hook holds every running window
(holdsUndying, also what 信仰搅拌机 S2 steps aside for), so a window outlasts the lend. M3茧甲 and 埃芒加德 revive in place
for PRTS's 0-time / 0-cost redeploy and now open a new deployment (revivedInPlace), like 阿戈尔 5's real redeploy.
This commit is contained in:
sganggs committed 2026-10-03 17:47:07 +08:00
1 parent 842e1e13c4
commit d6b1d1bd26
5 files changed
+187 -34

No files matched your search

+4 -2
View File
@@ -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 <bond_id> operator knocked down ⇒ +its tier (等阶) <bond_id> 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 });
},
+51 -23
View File
@@ -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)
},
+3 -2
View File
@@ -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;
+102 -6
View File
@@ -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');
}
});
+27 -1
View File
@@ -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);
});