fix: correct community-reported combat and strategy draft behavior
Guard lethal stuns against a replacement deployment; keep bard auras running through stun; require an active Aegir bond for Clementia's strategy; allow Compass SP to bypass recovery locks; and refresh Hidden Core gun mitigation against current gun/spring companions. Set multiplayer strategy picks to 50s and describe renderer fallback without diagnosing unsupported hardware. Golden moves (full 285-scenario regeneration): - roster-008: inactive Aegir no longer gains 18 strategy layers. - boss-boss_4-solo: bard regeneration continues through stun. - hidden-boss_9-pair: mitigation covers first hits/current companions. - standin-07: hammer stun cannot follow an immediate revival deployment. No ending-reason changes. Sources and report-by-report findings: DESIGN 28.25.
This commit is contained in:
1 parent
77cd29f07a
commit
708caf7d06
37 files changed
+277
-103
No files matched your search
@@ -572,18 +572,22 @@ test('埃芒加德 命结之秘: the first 3 knock-downs of the battle revive at
|
||||
cover('band_ermengard');
|
||||
});
|
||||
|
||||
test('克莱门莎 崇高牺牲: a <阿戈尔> operator knocked down ⇒ +its tier <阿戈尔> layers (no activation needed; none in 联防/boss)', () => {
|
||||
test('克莱门莎 崇高牺牲: a <阿戈尔> operator knocked down ⇒ +its tier <阿戈尔> layers (requires activation; none in 联防/boss)', () => {
|
||||
const ops = { t_eg: op('t_eg', { bonds: ['egirShip'], tier: 3 }), t_x: op('t_x', { bonds: ['yanShip'], tier: 5 }) };
|
||||
const h = fight({ band: 'band_clementia', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }, { chessId: 't_x', row: 11, col: 4 }], foes: [[10, 9]] });
|
||||
const h = fight({ band: 'band_clementia', bonds: active('egirShip'), ops, units: [{ chessId: 't_eg', row: 10, col: 4 }, { chessId: 't_x', row: 11, col: 4 }], foes: [[10, 9]] });
|
||||
h.step(1);
|
||||
h.b.dealDamage(foe(h), h.unit('t_eg'), { amount: 1e6, type: 'true' });
|
||||
h.b.dealDamage(foe(h), h.unit('t_x'), { amount: 1e6, type: 'true' });
|
||||
h.b.retreat(h.unit('t_eg')); // not knocked down: nothing (already dead anyway)
|
||||
assert.deepEqual(h.b.result().perPlayer.p1.layerGains, { egirShip: 3 });
|
||||
const u = fight({ band: 'band_clementia', kind: 'unite', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }], foes: [[10, 9]] });
|
||||
const u = fight({ band: 'band_clementia', bonds: active('egirShip'), kind: 'unite', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }], foes: [[10, 9]] });
|
||||
u.step(1);
|
||||
u.b.dealDamage(foe(u), u.unit('t_eg'), { amount: 1e6, type: 'true' });
|
||||
assert.deepEqual(u.b.result().perPlayer.p1.layerGains, {});
|
||||
const inactive = fight({ band: 'band_clementia', ops, units: [{ chessId: 't_eg', row: 10, col: 4 }], foes: [[10, 9]] });
|
||||
inactive.step();
|
||||
inactive.b.dealDamage(foe(inactive), inactive.unit('t_eg'), { amount: 1e6, type: 'true' });
|
||||
assert.deepEqual(inactive.b.result().perPlayer.p1.layerGains, {}, 'an inactive 阿戈尔 bond gains no layers');
|
||||
cover('band_clementia');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Community reports of 2026-10-10: actual attacks / skills, including death and redeployment in the same tick.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const bond = (layers = 0) => ({ count: 3, active: true, tier: 1, layers });
|
||||
const dummy = enemyRec({ key: 'dummy', hp: 1e9, atk: 0, speed: 0, mass: 0 });
|
||||
const wall = (id, opts = {}) => chessRec({ id, skill: null, stats: { maxHp: 1e7, atk: 0, def: 0, blockCnt: 3 }, ...opts });
|
||||
const finish = (h) => { checkInvariants(h.b); assert.deepEqual(h.b.errors, []); };
|
||||
const near = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} != ${expected}`);
|
||||
|
||||
for (const key of ['enemy_1045_hammer', 'enemy_1045_hammer_2', 'enemy_1160_hvyslr', 'enemy_1160_hvyslr_2']) {
|
||||
test(`${key}: a lethal stunning strike cannot stun the deployment created by 不屈`, () => {
|
||||
const h = makeBattle({ autoFinish: false, timeLimit: 100, defs: { chess: { wall: wall('wall') } },
|
||||
units: [{ chessId: 'wall', row: 10, col: 5 }], bonds: { indomShip: bond(300) } });
|
||||
const nth = key.includes('hvyslr') ? 4 : 3;
|
||||
h.step(); const u = h.unit('wall'); const e = h.spawn(key, { pos: [10, 5], mods: { speedMul: 0 } });
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= nth - 1, 40));
|
||||
const seq = u.deploySeq; u.hp = 1;
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= nth, 20));
|
||||
assert.ok(u.alive && u.deploySeq > seq, 'the lethal stunning strike immediately redeployed the operator');
|
||||
assert.ok(!u.s.flags.stun, 'the old attack cannot put stun on that new deployment');
|
||||
// The next stunning strike that does not kill must still stun normally.
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= nth * 2, 40));
|
||||
assert.ok(u.s.flags.stun); finish(h);
|
||||
});
|
||||
}
|
||||
|
||||
for (const skadi of [false, true]) {
|
||||
test(`${skadi ? '浊心斯卡蒂 custom' : 'default'} bard keeps its regeneration while stunned, stops after leaving`, () => {
|
||||
const bard = chessRec({ id: 'bard', profession: 'SUPPORT', subProfessionId: 'bard', attackKind: 'none', dmgType: 'heal',
|
||||
skill: null, rangeGrid: [[0, 0], [0, 1]], stats: { atk: 1000 } });
|
||||
const h = makeBattle({ autoFinish: false, defs: { chess: { bard, wall: wall('wall') } },
|
||||
units: [{ chessId: skadi ? 'chess_char_6_04_a' : 'bard', row: 10, col: 4, skillIndex: 1 }, { chessId: 'wall', row: 10, col: 5 }] });
|
||||
h.step(); const u = h.b.allyUnits[0], a = h.unit('wall'); a.hp = 100;
|
||||
if (skadi) { u.skill.gainSp(999, 'init'); assert.ok(u.skill.activate('test')); }
|
||||
h.run(1); const regen = a.s.hpRegen; assert.ok(regen > 0);
|
||||
h.b.applyStatus(u, 'stun', { duration: 10 }); h.run(2);
|
||||
near(a.s.hpRegen, regen); const hp = a.hp; h.run(1); assert.ok(a.hp > hp);
|
||||
h.b.kill(u, null); h.run(1); near(a.s.hpRegen, 0); finish(h);
|
||||
});
|
||||
}
|
||||
|
||||
test('黄沙罗盘 + 浓茶 bypass both the active skill lock and noSp, including the caster; ordinary SP still obeys them', () => {
|
||||
const op = (id, bonds) => chessRec({ id, bonds, skill: { spCost: 100, initSp: 0, duration: 20 } });
|
||||
const h = makeBattle({ autoFinish: false, defs: { chess: { a: op('a', ['sargonShip']), b: op('b', ['sargonShip']), c: op('c', []) } },
|
||||
units: [{ chessId: 'a', row: 10, col: 4, items: ['chess_item_6_07_e_a', 'chess_item_2_04_e_a'] },
|
||||
{ chessId: 'b', row: 11, col: 4 }, { chessId: 'c', row: 12, col: 4 }] });
|
||||
h.step(); const a = h.unit('a'), b = h.unit('b'), c = h.unit('c');
|
||||
for (const u of [a, b, c]) { u.skill.gainSp(100, 'init'); h.b.addBuff(u, { key: 'test:lock', flags: { noSp: true } }); }
|
||||
assert.ok(b.skill.activate('test')); assert.ok(c.skill.activate('test')); assert.ok(a.skill.activate('test'));
|
||||
near(a.skill.sp, 3); near(b.skill.sp, 3); near(c.skill.sp, 0);
|
||||
near(b.skill.gainSp(5, 'item'), 0); near(b.skill.sp, 3);
|
||||
a.skill.end('test'); near(a.skill.sp, 33); finish(h);
|
||||
});
|
||||
|
||||
for (const elite of [false, true]) {
|
||||
test(`蕾缪安 S3 real ammo consumption gains active bonds with three operators in the row (${elite ? 'elite' : 'normal'})`, () => {
|
||||
const id = `chess_char_6_01_${elite ? 'b' : 'a'}`;
|
||||
const h = makeBattle({ autoFinish: false, timeLimit: 120, hooks: ['ammoUsed'],
|
||||
defs: { chess: { a: wall('a'), b: wall('b') }, enemies: { dummy } },
|
||||
bonds: { lateranoShip: bond(), preciShip: bond() },
|
||||
units: [{ chessId: id, row: 10, col: 4 }, { chessId: 'a', row: 10, col: 2 }, { chessId: 'b', row: 10, col: 3 }] });
|
||||
h.step(); const u = h.unit(id); h.spawn('dummy', { pos: [10, 6] });
|
||||
for (let i = 0; i < 2; i++) { u.skill.gainSp(999, 'init'); assert.ok(u.skill.activate('test')); assert.ok(h.runUntil(() => !u.skill.active, 30)); }
|
||||
const spent = h.hooksOf('ammoUsed').filter((c) => c.unit === u).length;
|
||||
assert.ok(spent >= 10); const gains = h.result().perPlayer.p1.layerGains;
|
||||
for (const b of ['lateranoShip', 'preciShip']) assert.equal(gains[b], Math.floor(spent / 10) * (elite ? 4 : 2), b);
|
||||
finish(h);
|
||||
});
|
||||
}
|
||||
|
||||
for (const id of ['chess_char_4_23_a', 'chess_char_5_18_a']) {
|
||||
test(`${id} 百炼嘉维尔 S2 damages a final boss but cannot pull it`, () => {
|
||||
const sharedBoss = { hp: 1e9, maxHp: 1e9, damage(pid, n) { this.hp -= n; } };
|
||||
const h = makeBattle({ kind: 'boss', sharedBoss, autoFinish: false, hooks: ['damaged'], captureNoisy: true,
|
||||
units: [{ chessId: id, row: 10, col: 3, skillIndex: 1 }] });
|
||||
h.step(); const u = h.unit(id);
|
||||
const e = h.spawn('enemy_9017_achunt', { pos: [3, 5], tag: 'boss', mods: { speedMul: 0 } });
|
||||
e.profile.noAttack = true;
|
||||
u.skill.gainSp(999, 'init'); assert.ok(u.skill.activate('test')); h.run(3);
|
||||
assert.ok(h.hooksOf('damaged').some((c) => c.source === u && c.target === e), 'S2 actually hits the leader');
|
||||
near(e.x, 5); near(e.y, 3);
|
||||
// An excessive force isolates the leader exclusion from its ordinary high weight.
|
||||
near(h.b.pullToFront(e, u, 99), 0); near(e.x, 5); near(e.y, 3); finish(h);
|
||||
});
|
||||
}
|
||||
@@ -2713,6 +2713,23 @@ test('假想敌:铳 (h07_02 override) never uses 最终之罚 in the regular b
|
||||
assert.ok(!h.eventsOf('fx').some((f) => f[1] === 'charge'));
|
||||
});
|
||||
|
||||
test('隐秘核心 铳: another gun alone grants 80% reduction from the very first hit; no stale reduction after it leaves', () => {
|
||||
const h = bossArena({ units: [{ chessId: 't_wall', row: 10, col: 7 }] });
|
||||
h.step();
|
||||
const g = put(h, 'enemy_9017_achunt_2', [3, 10], { tag: 'boss' });
|
||||
const other = put(h, 'enemy_9017_achunt_2', [3, 4], { tag: 'boss' });
|
||||
const full = 1000 * (1 - g.s.res / 100);
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full * 0.2);
|
||||
// Leaving the field without killing the shared boss pool isolates the companion condition.
|
||||
other.alive = false; other.deployed = false;
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full);
|
||||
const spring = put(h, 'enemy_9020_actrpc', [3, 6], { tag: 'part' });
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full * 0.2);
|
||||
spring.alive = false; spring.deployed = false;
|
||||
approx(h.b.dealDamage(null, g, { amount: 1000, type: 'arts' }), full);
|
||||
checkInvariants(h.b); assert.deepEqual(h.b.errors, []);
|
||||
});
|
||||
|
||||
test('假想敌:铳 (隐秘核心): damage ×0.2 while springs live; 盲信之誓 lines hurt operators on them; 末日布道 dash', () => {
|
||||
const h = bossArena({ units: [{ chessId: 't_wall', row: 10, col: 7 }], setup(b) { b.enemyOverrides = W.act1autochess_h08_02.overrides; } });
|
||||
h.step();
|
||||
|
||||
@@ -617,7 +617,7 @@ test('黄沙罗盘: initial SP +30 / +50; (萨尔贡) first skill end +30 SP; +
|
||||
u.skill.sp = 0;
|
||||
u.skill.activate('test', { free: true });
|
||||
u.skill.end('test');
|
||||
close(u.skill.sp, 0, 'only the first skill end');
|
||||
close(u.skill.sp, 3, 'only the first skill end gives 30; this cast still gives its own 3 through the lock');
|
||||
cover(A('6_07'), B('6_07'), A('2_04'), B('2_04'));
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user