fix(sim): 浊心斯卡蒂 S1 and 坚守 tier 2 no longer bounce redirected damage between each other (the battle froze: every cycle branched per 坚守 member until the hook depth guard); regression test; golden: unchanged

This commit is contained in:
HappyTanuki committed 2026-10-10 11:06:10 +09:00
1 parent 1db8e51023
commit e9a2e27c58
3 files changed
+55 -2

No files matched your search

+3 -1
View File
@@ -542,7 +542,9 @@ export function install(battle) {
if (stead) { if (stead) {
battle.on('hit', (c) => { battle.on('hit', (c) => {
const t = c.target, dmg = c.dmg; const t = c.target, dmg = c.dmg;
if (!t || t.side !== 'ally' || t.kind !== 'op' || !dmg || dmg.cancel || dmg.steadShare || dmg.steadCut || dmg.type === 'element') return; // redirected damage (浊心斯卡蒂 S1 transfer) is not cut and re-shared again (see chess_char_6_04-skadi2.js)
// [ASSUMED: the official game does not chain the two transfers either]
if (!t || t.side !== 'ally' || t.kind !== 'op' || !dmg || dmg.cancel || dmg.steadShare || dmg.steadCut || dmg.type === 'element' || dmg.tags?.includes('transfer')) return;
const st = byPid[t.ownerId]; const st = byPid[t.ownerId];
if (!st || (st.tiers[ID.stead] ?? 0) < 2 || st.members[ID.stead].has(t)) return; if (!st || (st.tiers[ID.stead] ?? 0) < 2 || st.members[ID.stead].has(t)) return;
let any = false; let any = false;
@@ -121,7 +121,10 @@ function skadi2(bb, chess, def) {
battle.on('hit', (ctx) => { battle.on('hit', (ctx) => {
const t = ctx.target, d = ctx.dmg; const t = ctx.target, d = ctx.dmg;
if (!(share > 0) || !t || t === unit || t.side !== 'ally' || t.kind === 'device' || !live(unit) || !unit.skill?.active) return; if (!(share > 0) || !t || t === unit || t.side !== 'ally' || t.kind === 'device' || !live(unit) || !unit.skill?.active) return;
if (d.type === 'element' || d.type === 'elemental' || d.skadiShare || d.tags?.includes('transfer') || !inCover(battle, unit, t)) return; // damage another sharing effect already redirected (坚守 bond:stead:share) is not redirected again: the two
// would bounce it between Skadi and the 坚守 members until the hook depth guard (docs/SIM.md §5 re-entrancy
// rule) [ASSUMED: the official game does not chain the two transfers either]
if (d.type === 'element' || d.type === 'elemental' || d.skadiShare || d.steadShare || d.tags?.includes('transfer') || d.tags?.includes('bond:stead:share') || !inCover(battle, unit, t)) return;
d.skadiShare = { share, tag: TAG }; // 同类效果取最高: one transfer per damage instance d.skadiShare = { share, tag: TAG }; // 同类效果取最高: one transfer per damage instance
d.mul *= 1 - share; d.mul *= 1 - share;
}, { owner: unit, priority: -20 }); }, { owner: unit, priority: -20 });
+48
View File
@@ -0,0 +1,48 @@
// Regression: 浊心斯卡蒂 S1 (damage transfer to her) + 坚守 tier 2 (damage cut and shared to the 坚守 members) used to
// bounce redirected damage between Skadi and the members — every cycle branching once per member — until the hook
// depth guard: k members ≈ k^16 damage events per hit, which froze a boss field (an automated bot run, seed 2000301
// solo HARD). Redirected damage is now never redirected again.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { getDefaultSource } from '../../server/sim/simdata.js';
import { bondBb } from '../../server/sim/content/bonds/addon/battle.js';
const ds = getDefaultSource();
const SID = 'skchr_skadi2_1';
const op = (id, bonds) => chessRec({ id, bonds, profession: 'WARRIOR', skill: null, stats: { def: 0, maxHp: 1e6 } });
const bond = (tier, layers = 0, count = 3) => ({ count, active: tier > 0, tier, layers });
for (const id of ['chess_char_6_04_a', 'chess_char_6_04_b']) {
test(`${id}: Skadi S1 transfer + 坚守 tier 2 share do not bounce redirected damage`, () => {
const skillIndex = ds.rawChess(id).skills.find((s) => s.skillId === SID).index;
const share = ds.rawChess(id).skills.find((s) => s.skillId === SID).bb.damage_resistance;
const cut = bondBb('steadShip').damage_resistance;
const h = makeBattle({
seed: 7, autoFinish: false, timeLimit: 400, captureNoisy: true, hooks: ['damaged'],
defs: { chess: { d_1: op('d_1', ['steadShip']), d_2: op('d_2', ['steadShip']), d_3: op('d_3', ['steadShip']), d_n: op('d_n', []) },
enemies: { dummy: enemyRec({ key: 'dummy', hp: 1e7, speed: 0 }) } },
// Skadi and four allies next to her (in her range), three of them 坚守 members
units: [{ chessId: id, row: 10, col: 4, skillIndex, carryState: { sp: 999 } },
{ chessId: 'd_1', row: 10, col: 5 }, { chessId: 'd_2', row: 10, col: 3 }, { chessId: 'd_3', row: 11, col: 4 }, { chessId: 'd_n', row: 9, col: 4 }],
bonds: { steadShip: bond(2, 10) },
enemies: [{ key: 'dummy', pos: [9, 9] }],
});
const u = h.unit(id);
if (u.skill && !u.skill.active) h.b.startSkill?.(u);
h.step(2);
assert.ok(u.skill?.active, 'S1 active');
const t0 = Date.now();
const n0 = h.hooksOf('damaged').length;
h.b.dealDamage(h.enemy('dummy'), h.unit('d_n'), { amount: 1000, type: 'true' });
const events = h.hooksOf('damaged').length - n0;
assert.ok(Date.now() - t0 < 500, `one hit resolves at once (${Date.now() - t0} ms)`);
assert.ok(events <= 8, `a bounded number of damage events (${events})`);
assert.equal(h.b.errors.filter((e) => /hookDepth/.test(String(e.key ?? e.label ?? JSON.stringify(e)))).length, 0, 'no hook-depth loop');
// the non-member's hit: Skadi takes her share once; 坚守 shares the cut of the rest once, nothing is redirected twice
const dn = h.unit('d_n');
assert.ok(dn.s.maxHp - dn.hp > 0 && dn.s.maxHp - dn.hp < 1000, 'the ally keeps part of the hit');
assert.ok(share > 0 && cut > 0);
checkInvariants(h.b);
});
}