feedback5 O5: damage.js gets the hpDamage hook — what passed the shields of a damage instance, before the HP loss; a handler may only lower it (a raise is ignored), a 流失 and a hit the shields absorb whole never reach it

The 伤判效果 that act after a barrier need it: 煌's 紧急除颤 lock (buff template huang_t_1[lock], an ON_TAKE_DAMAGE HpNoLessThanCertainPercentModifier; PRTS 备注 "该伤害减少(伤害值-煌当前生命值+煌最大生命值×50%)点") and 左乐's SBL-X 庇护 re-applied behind his 行险 barrier (PRTS S2 备注). Additive: without a handler the pipeline is unchanged, so golden is unchanged (every family matches). docs/SIM.md §4 / §5 name the hook; test/sim/feedback5-hpdamage-hook.test.js (2).
This commit is contained in:
sganggs committed 2026-10-06 00:07:06 +08:00
1 parent 742f478526
commit 89d17d8e08
3 files changed
+68 -3

No files matched your search

+4 -1
View File
@@ -678,7 +678,9 @@ PRTS 异常效果: 无法选择 effects "仅在选择时生效"); checked before
with `ceil(final) ≥ BOSS_HIT_LIMIT` (300000, shared/constants.js) is cancelled whole: returns 0 before shields (阿利斯泰尔's
`boss:vest` barrier stays untouched; a `hit`-step block such as 假想敌:再生's aura acts earlier) [ASSUMED order], no HP /
pool loss, no credit or stats, no `dmg` event, no `damaged` / `fatal` / kill; an fx `hitCap` `{ id, n }` marks it and
draws nothing; research 11) → shields → HP loss
draws nothing; research 11) → shields → **`hpDamage`** (what passed the shields; a handler may only lower `amount` — the
伤判效果 that act after a barrier: 煌's 紧急除颤 HP floor, PRTS 备注 "该伤害减少(伤害值-煌当前生命值+煌最大生命值×50%)点";
左乐's 庇护 re-applied after his 行险 barrier; never for a 流失) → HP loss
(boss units: routed to `sharedBoss.damage(playerId, amount)`; a pool left under 1 HP is emptied) → if HP ≤ 0: **`fatal`** (`ctx.prevented = true` keeps the
unit at ≥ 1 HP) → **`damaged`** → SP-on-hurt / TAKE_DAMAGE → `kill` + `death`.
@@ -735,6 +737,7 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO
| `hit` | `{ source, target, dmg, credit }` | before mitigation; mutate `dmg` (not fired for gauge fills — see `elementHit`). `source` may be null (terrain; 无来源 `dmg.sourceless` bursts, whose `credit` names the unit credited) |
| `elementHit` | `{ source, target, dmg }` | before a gauge fill (`dmg.type === 'element'`); mutate `dmg.amount`/`dmg.mul`, set `dmg.cancel` |
| `damaged` | `{ source, target, amount, type, dmg, credit }` | after application (`amount` may be 0 when shielded); element fills too (with their source); 无来源: `source` null, `credit` set |
| `hpDamage` | `{ source, target, amount, dmg, credit }` | a damage instance after shields, before the HP loss (§4; not a 流失): lower `amount` only (a raise is ignored) — HP floors / reductions ordered after a barrier (kits/ops/op-huang.js, op-zuole.js) |
| `heal` | `{ source, target, amount, opts }` | mutable `amount` |
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — set `prevented` (substitutes, kit savers, 不死 / 复活 items, 埃芒加德; 不屈 is a `death` hook). Fired by every HP loss of a unit without a boss pool — hits of any type, element bursts, 无来源 damage, `loseHp` 流失. Order: kits' own savers (10 … −60) → items' 不死 (坚固维式重锤 — once per deployment: `items/battle.js deploymentOf`, a key every deploy changes and an in-place 复活 changes too; one battle-level hook holds the running windows (`holdsUndying`), so a window outlasts a lend, DESIGN §21.21 — the lock `PRIO_REVIVE` −100 after the substitutes (−100, registered first), the running windows `PRIO_UNDYING_HELD` −99 before them: a 傀儡师 holding 不死 does not switch, PRTS 分支特性信息 傀儡师 "未持有不死的情况下", DESIGN §22.11) → items' 复活 (M3茧甲, `PRIO_RESPAWN` −101: PRTS "复活" acts on a knock-out, which a 不死 prevents) → 埃芒加德 (−110); both 复活 revive in place and call `revivedInPlace` (a new deployment for the lock) |
| `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` |
+9 -2
View File
@@ -5,8 +5,9 @@
// → dodge (phys/arts, canDodge) → mitigation (phys: DEF, arts: RES, true: none)
// → × source dmgDealtMul (× phys/artsDealtMul) × target dmgTakenMul (not for 元素伤害) × type-taken mul × dmg.mul
// → 限伤 (leaders in boss / hidden battles: a hit of ceil(final) ≥ BOSS_HIT_LIMIT is cancelled, see leaderHitCancelled)
// → shields (hit-negating barriers first, then HP shields) → HP loss (boss pool routing) → 'damaged' hook
// → SP-on-hurt / TAKE_DAMAGE trigger → fatal/kill.
// → shields (hit-negating barriers first, then HP shields) → 'hpDamage' hook (what passed the shields; handlers may
// only lower `amount`: the 伤判效果 that act after a barrier — 煌's 紧急除颤 HP floor, 左乐's 庇护 re-applied after his
// 行险 barrier) → HP loss (boss pool routing) → 'damaged' hook → SP-on-hurt / TAKE_DAMAGE trigger → fatal/kill.
// Damage-dealt stats (the source's `stats.dmg`, the player's `damageDealt`) count only HP removed from the other side:
// self and friendly damage (a 源石溶剂 drain, an operator's own 流失) is the target's `taken` and keeps the kill credit.
// Phys: max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 5 %·A); Arts: max(A×(1 − R′/100), 5 %·A) with
@@ -268,6 +269,12 @@ export function dealDamage(battle, source, target, dmgIn) {
// ran before it (the attack, its SP, `hit` hook effects, separate element 损伤) stays; nothing after it happens
if (final > 0 && leaderHitCancelled(battle, target, final)) return 0;
final = absorbShields(battle, target, final);
// 伤判效果 after the barriers (header): a handler may lower what reaches the HP — never raise it (a 流失 skips this)
if (final > 0 && battle._hooks.hpDamage) {
const hctx = { source: hs, target, amount: final, dmg, credit: source };
battle.emit('hpDamage', hctx);
if (Number.isFinite(hctx.amount)) final = Math.max(0, Math.min(final, hctx.amount));
}
return applyHpLoss(battle, source, target, final, dmg);
}
+55
View File
@@ -0,0 +1,55 @@
// test/sim/feedback5-hpdamage-hook.test.js — the `hpDamage` hook of damage.js dealDamage (0.2.0, the 自选 operator kits):
// what passed the shields of a damage instance, before the HP loss; a handler may only lower it — the 伤判效果 that act
// after a barrier (煌's 紧急除颤 HP floor, 左乐's 庇护 re-applied behind his 行险 barrier). A 流失 (Battle.loseHp) never
// reaches it, a hit the shields absorb whole neither. docs/SIM.md §4 / §5.
// Run: node --test test/sim/feedback5-hpdamage-hook.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
function arena() {
const h = makeBattle({
defs: { enemies: { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e6, speed: 0, mass: 0 }) } },
units: [{ uid: 1, chessId: 'chess_char_1_02_a', row: 10, col: 5 }], timeLimit: 60, autoFinish: false,
});
h.step();
const u = h.unit(1), e = h.spawn('enemy_dummy', { pos: [10, 8] });
return { h, u, e };
}
test('hpDamage: after the shields, before the HP loss — a handler lowers what reaches the HP; a raise is ignored', () => {
const { h, u, e } = arena();
const seen = [];
const off = h.b.on('hpDamage', (c) => { if (c.target === u) { seen.push([c.amount, c.source, c.dmg.type]); c.amount = Math.min(c.amount, 100); } });
h.b.addBuff(u, { key: 'test:shield', shield: 300 });
const hp = u.hp;
const dealt = h.b.dealDamage(e, u, { amount: 1000, type: 'true' });
assert.deepEqual(seen, [[700, e, 'true']], 'the 300 shield first, then the hook sees the 700 that passed');
assert.equal(u.findBuff('test:shield'), null, 'the shield was spent');
assert.deepEqual([hp - u.hp, dealt], [100, 100], 'HP loss and the returned amount follow the lowered value');
h.b.off(off);
h.b.on('hpDamage', (c) => { if (c.target === u) c.amount = 1e9; });
const hp2 = u.hp;
h.b.dealDamage(e, u, { amount: 50, type: 'true' });
assert.equal(hp2 - u.hp, 50, 'a raise is ignored');
checkInvariants(h.b);
});
test('hpDamage: no call for a 流失 nor for a hit the shields take whole; without a handler nothing changes', () => {
const { h, u, e } = arena();
let n = 0;
h.b.on('hpDamage', (c) => { if (c.target === u) n++; });
h.b.loseHp(u, 100, { source: e });
assert.equal(n, 0, 'a 流失 skips it (no 伤判效果)');
h.b.addBuff(u, { key: 'test:shield', shield: 500 });
h.b.dealDamage(e, u, { amount: 200, type: 'true' });
assert.equal(n, 0, 'absorbed whole: nothing reaches the HP');
h.b.dealDamage(e, u, { amount: 400, type: 'true' });
assert.equal(n, 1, 'the 100 that passed');
const r = arena();
const hp = r.u.hp;
r.h.b.dealDamage(r.e, r.u, { amount: 250, type: 'true' });
assert.equal(hp - r.u.hp, 250, 'no handler: the plain pipeline');
checkInvariants(h.b);
});