feedback5 CB1: a 复活 (埃芒加德's 命结之秘, M3茧甲) is a knock-out and an immediate free redeploy — 砾's and 瑕光's 被击倒时 / 部署时 layers fire on every revive (community report 「卡西米尔和艾芒加德策略似乎有BUG:像砾和瑕光这种死亡和部署的叠层效果…无法触发」, items 9 / 37)
PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 埃芒加德 and M3茧甲 备注: "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的 再部署时间和费用归零". Both revived in place as `fatal` savers, so the operator was never knocked out nor deployed again: 砾 (<部署时> 卡西米尔 +1, <被击倒时> 不屈 +2), 瑕光 (<部署时> +4, 12 per battle), the 卡西米尔 bond's per-deployment ATK, 克莱门莎 / 卡莱莎 and every other 被击倒时 / 部署时 effect skipped the first 3 knock-outs (measured: only the 4th, real death stacked). Now items/battle.js reviveNow, a `death` hook (M3茧甲 PRIO_RESPAWN 13, 埃芒加德 PRIO_BAND_REVIVE 12, ahead of 阿戈尔 5 at 11 and 不屈 at 10): the knock-out stands and the operator redeploys at once, free, where it lies (full HP, SP reset, a new deployment for 坚固维式重锤's lock — revivedInPlace and the `revives` counter are gone). The ctx carries `revivedBy`, so 阿戈尔 5 still does not count such a knock-out as the member's first (the owner's decision of 2026-10-05: an operator's own revive uses no slot; 埃芒加德 [ASSUMED] alike, unchanged). 淬羽赫默 S3 keeps its place right after the hammer's lock (PRIO_REVIVE − 0.5). Tests: test/content/feedback5-revive-redeploy.test.js (fails on the parent); bands / bonds_core / feedback1f-hammer / op_lessng / op_zuole / docs-consistency updated to the redeploy. Docs: SIM.md (fatal, death), design/engine.md (fatal, death), PLAYING.md (复活), research 04 (M3茧甲). Golden: the battles with M3茧甲 or 埃芒加德 whose operators fell — a revive is now a knock-out (deaths count, kill events, redeploy, SP reset, 被击倒时 / 部署时 effects): roster-012 / 026 / 033 / 040 / 047, boss-boss_6-pair, hidden-boss_8-pair / boss_9-pair, standin-07; matches solo-NORMAL-1, solo-HARD-1, solo-ABYSS-1.
This commit is contained in:
1 parent
4fd8d431e2
commit
a3a0ddd2be
19 files changed
+464
-344
No files matched your search
@@ -8,7 +8,7 @@ import { makeMatch, give, giveItem, DATA } from '../match/harness.js';
|
||||
import { createRegistry } from '../../server/match/effectsMeta.js';
|
||||
import { hasBattlePart, amedicCharsFor, PRIO_BAND_REVIVE } from '../../server/sim/content/bands/battle.js';
|
||||
import { bandMetaHandler, duckReplace } from '../../server/sim/content/bands/meta.js';
|
||||
import { PRIO_REVIVE } from '../../server/sim/content/items/battle.js';
|
||||
import { PRIO_RESPAWN } from '../../server/sim/content/items/battle.js';
|
||||
import * as bands from '../../server/sim/content/bands.js';
|
||||
|
||||
const QUIET = { warn() {}, error() {}, info() {} };
|
||||
@@ -532,9 +532,12 @@ test('阿米娅 众志合一: 3 / 4 / ≥ 5 active bonds ⇒ every operator ATK
|
||||
cover('band_amiya');
|
||||
});
|
||||
|
||||
test('埃芒加德 命结之秘: the first 3 knock-downs of the battle revive at full HP (after the operators\' own revive items)', () => {
|
||||
test('埃芒加德 命结之秘: the first 3 knock-downs of the battle revive at full HP (after the operators\' own revive items) — a knock-out and a free redeploy', () => {
|
||||
const ops = { t_op: op('t_op'), t_b: op('t_b') };
|
||||
const h = fight({ band: 'band_ermengard', ops, units: [{ chessId: 't_op', row: 10, col: 4, items: [A('4_12')] }, { chessId: 't_b', row: 11, col: 4 }], foes: [[10, 9]] });
|
||||
const log = [];
|
||||
h.b.on('death', (c) => log.push(['death', c.unit.defId, c.revivedBy ?? null]), { priority: -1000 });
|
||||
h.b.on('deploy', (c) => { if (!c.initial && c.unit.side === 'ally') log.push(['deploy', c.unit.defId]); }, { priority: -1000 });
|
||||
h.step(1);
|
||||
const [a, b] = [h.unit('t_op'), h.unit('t_b')];
|
||||
const kill = (u) => h.b.dealDamage(foe(h), u, { amount: 1e6, type: 'true' });
|
||||
@@ -543,9 +546,13 @@ test('埃芒加德 命结之秘: the first 3 knock-downs of the battle revive at
|
||||
kill(a); kill(b); kill(b); // band revives 1, 2, 3
|
||||
assert.ok(a.alive && b.alive);
|
||||
close(b.hp, 2000, 'full HP');
|
||||
// PRTS 备注 "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的再部署时间和费用归零": each revive is a knock-out
|
||||
// (death hooks — 被击倒时 effects) and an immediate free redeploy where it lies (deploy hooks — 部署时 effects)
|
||||
assert.deepEqual(log.filter((x) => x[0] === 'death').map((x) => x.slice(1)), [['t_op', 'item'], ['t_op', 'band'], ['t_b', 'band'], ['t_b', 'band']]);
|
||||
assert.deepEqual(log.filter((x) => x[0] === 'deploy').map((x) => x[1]), ['t_op', 't_op', 't_b', 't_b'], 'each knock-out redeployed at once');
|
||||
kill(a);
|
||||
assert.equal(a.alive, false, '4th knock-down of the band: dies');
|
||||
assert.ok(PRIO_BAND_REVIVE < PRIO_REVIVE);
|
||||
assert.ok(PRIO_BAND_REVIVE < PRIO_RESPAWN, 'the band after the item revive (both death hooks, after every fatal saver)');
|
||||
cover('band_ermengard');
|
||||
});
|
||||
|
||||
|
||||
@@ -542,7 +542,7 @@ test('阿戈尔 5: a member devoured by several 阿戈尔 spends one slot at mos
|
||||
checkInvariants(t.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 5: an operator\'s own save uses no slot (斯卡蒂\'s DRE-Y, M3茧甲, 埃芒加德 [ASSUMED for the band]); the slot waits for its first real knock-out', () => {
|
||||
test('阿戈尔 5: an operator\'s own save or revive uses no slot (斯卡蒂\'s DRE-Y, M3茧甲, 埃芒加德 [ASSUMED for the band]); the slot waits for its first unanswered knock-out', () => {
|
||||
// 斯卡蒂 (elite, module DRE-Y by default) enters 联防 at half HP in front of 乌尔比安: his mark is lethal, DRE-Y saves her
|
||||
// (PRTS: once per deployment, full HP with max HP −60 %) — no knock-out, so no slot
|
||||
const SKADI = 'chess_char_3_05_b', ULPIA = 'chess_char_5_05_a', GHOST = 'chess_char_2_07_a', HN = 'chess_char_3_09_a', DEEP = 'chess_char_1_04_a';
|
||||
@@ -568,20 +568,23 @@ test('阿戈尔 5: an operator\'s own save uses no slot (斯卡蒂\'s DRE-Y, M3
|
||||
const list = [['g0_a', ['egirShip']], ['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']], ['g4_a', ['egirShip']]];
|
||||
const units = (it = {}) => list.map(([chessId], i) => ({ chessId, row: 9 + (i % 2) * 3, col: 3 + i, items: it[i] }));
|
||||
const killed = (b, id) => { const u = b.unit(id); b.b.dealDamage(null, u, { amount: 1e9, type: 'true' }); return u; };
|
||||
// M3茧甲 (an item 复活, in place): g0 is saved, the 3 slots go to g1, g2, g3; g4 stays down
|
||||
// M3茧甲 (an item 复活 — since 0.2.0 PRTS's form: a knock-out answered by a free redeploy, items reviveNow): g0 is knocked
|
||||
// out and back at once, the 3 slots go to g1, g2, g3; g4 stays down
|
||||
const hi = makeBattle({ defs: defsOf(list), autoFinish: false, timeLimit: 60, hooks: ['death', 'deploy'], units: units({ 0: ['chess_item_4_12_e_a'] }), bonds: { egirShip: bondOn(5, 0, null, [3, 5]) } });
|
||||
hi.step(1);
|
||||
const g0 = killed(hi, 'g0_a');
|
||||
assert.ok(g0.alive && hi.hooksOf('death').filter((c) => c.unit === g0).length === 0, 'item: saved in place (no knock-out)');
|
||||
assert.deepEqual(['g1_a', 'g2_a', 'g3_a', 'g4_a'].map((id) => killed(hi, id).alive), [true, true, true, false], 'item: three slots left after the save');
|
||||
assert.ok(g0.alive && hi.hooksOf('death').filter((c) => c.unit === g0 && c.reason === 'killed').length === 1, 'item: knocked out and revived');
|
||||
assert.equal(hi.hooksOf('death').find((c) => c.unit === g0).revivedBy, 'item');
|
||||
assert.deepEqual(['g1_a', 'g2_a', 'g3_a', 'g4_a'].map((id) => killed(hi, id).alive), [true, true, true, false], 'item: three slots left after the revive');
|
||||
checkInvariants(hi.b);
|
||||
// 埃芒加德 (命结之秘: the battle's first 3 knock-downs revive in place — its own count, unchanged): g0, g1, g2 saved by it;
|
||||
// the 阿戈尔 slots then go to g3, g4 and g0's first real knock-out; g1's real one finds none
|
||||
// 埃芒加德 (命结之秘: the battle's first 3 knock-downs revive at once — its own count, unchanged): g0, g1, g2 revived by it;
|
||||
// the 阿戈尔 slots then go to g3, g4 and g0's next knock-out (its first unanswered one); g1's finds none
|
||||
const hb = makeBattle({ defs: defsOf(list), autoFinish: false, timeLimit: 60, hooks: ['death', 'deploy'], units: units(), bandId: 'band_ermengard', bonds: { egirShip: bondOn(5, 0, null, [3, 5]) } });
|
||||
hb.step(1);
|
||||
for (const id of ['g0_a', 'g1_a', 'g2_a']) {
|
||||
const u = killed(hb, id);
|
||||
assert.ok(u.alive && hb.hooksOf('death').filter((c) => c.unit === u).length === 0, `band: ${id} saved in place`);
|
||||
const deaths = hb.hooksOf('death').filter((c) => c.unit === u);
|
||||
assert.ok(u.alive && deaths.length === 1 && deaths[0].revivedBy === 'band', `band: ${id} knocked out and revived by the band`);
|
||||
}
|
||||
assert.deepEqual(hb.eventsOf('fx').filter((x) => x[1] === 'revive' && x[4] && x[4].left != null).map((x) => x[4].left), [2, 1, 0], '埃芒加德 spent its 3');
|
||||
assert.deepEqual(['g3_a', 'g4_a', 'g0_a', 'g1_a'].map((id) => killed(hb, id).alive), [true, true, true, false], 'band: the 阿戈尔 slots are untouched');
|
||||
|
||||
@@ -156,7 +156,8 @@ test('F1 坚固维式重锤 in front of an 阿戈尔: the battle-start devour sp
|
||||
});
|
||||
|
||||
test('F1 坚固维式重锤 + M3茧甲: the lock comes before the revive whatever the equip order (PRTS: a revive acts on 退场)', () => {
|
||||
assert.ok(PRIO_RESPAWN < PRIO_REVIVE && PRIO_RESPAWN > PRIO_BAND_REVIVE, 'revive items: after every 不死, before 埃芒加德');
|
||||
// revive items: a `death` hook (after every 不死 — a fatal saver), before 埃芒加德's
|
||||
assert.ok(Number.isFinite(PRIO_REVIVE) && PRIO_RESPAWN > PRIO_BAND_REVIVE, 'revive items before 埃芒加德');
|
||||
for (const items of [[M3, HAMMER], [HAMMER, M3]]) {
|
||||
const c = carrier(items);
|
||||
const log = fxLog(c.h, c.u);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Community report of 2026-10-06 (items 9 / 37) 「卡西米尔和艾芒加德策略似乎有BUG:像砾和瑕光这种死亡和部署的叠层效果,如果有
|
||||
// 艾芒加德的3次复活似乎是无法触发…只有最后一次真的死了没站起来才叠层了」: 埃芒加德's 命结之秘 (and M3茧甲) revived in place — a
|
||||
// `fatal` saver — so no 被击倒时 and no 部署时 effect fired. PRTS (卫戍协议:盟约 下半/PRTS盟约记录, 埃芒加德 and M3茧甲 备注):
|
||||
// "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的再部署时间和费用归零" — the knock-out stands and the operator
|
||||
// redeploys at once, free: items/battle.js reviveNow, a `death` hook.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec } from '../helpers/battleHarness.js';
|
||||
import { hasGeneratedData } from '../../server/sim/simdata.js';
|
||||
|
||||
const REAL = { skip: !hasGeneratedData() };
|
||||
const GRAVEL = 'chess_char_2_12_a'; // 砾: <部署时>已激活的【卡西米尔】+1, <被击倒时>已激活的【不屈】+2
|
||||
const BLEMISH = 'chess_char_3_12_a'; // 瑕光: <部署时>自身已激活的盟约 +4 (每场作战至多12层)
|
||||
|
||||
function setup({ band = 'band_ermengard', items = {} } = {}) {
|
||||
const h = makeBattle({
|
||||
units: [{ chessId: GRAVEL, row: 9, col: 4, items: items[GRAVEL] }, { chessId: BLEMISH, row: 11, col: 4, items: items[BLEMISH] }],
|
||||
bonds: {
|
||||
kazimierzShip: { count: 3, layers: 1, active: true, tier: 1 },
|
||||
indomShip: { count: 2, layers: 1, active: true, tier: 1 },
|
||||
raidShip: { count: 2, layers: 1, active: true, tier: 1 },
|
||||
},
|
||||
bandId: band,
|
||||
enemies: [{ key: 'enemy_far', time: 0, route: 0 }],
|
||||
defs: { enemies: { enemy_far: enemyRec({ key: 'enemy_far', hp: 1e9, atk: 0, bat: 99, speed: 0.0001 }) } },
|
||||
timeLimit: 120, seed: 3,
|
||||
});
|
||||
const gains = [];
|
||||
h.b.on('layerGain', (c) => gains.push([c.source?.defId, c.bondId, c.n]), { priority: -1000 });
|
||||
h.step();
|
||||
const kill = (id) => { const u = h.unit(id); h.b.dealDamage(h.b.enemies[0], u, { amount: 1e7, type: 'true', canDodge: false }); return u; };
|
||||
const sum = (id, bond) => gains.filter(([s, b]) => s === id && b === bond).reduce((a, [, , n]) => a + n, 0);
|
||||
return { h, kill, sum, gains };
|
||||
}
|
||||
|
||||
test('埃芒加德: each of the 3 revives is a knock-out and a deployment — 砾 gains 不屈 +2 and 卡西米尔 +1 every time', REAL, () => {
|
||||
const { h, kill, sum } = setup();
|
||||
assert.equal(sum(GRAVEL, 'kazimierzShip'), 1, 'the battle-start deployment');
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const u = kill(GRAVEL);
|
||||
assert.ok(u.alive, `revive ${i}: standing again`);
|
||||
assert.equal(sum(GRAVEL, 'indomShip'), 2 * i, `revive ${i}: <被击倒时> 不屈 +2 (the parent: 0 until the 4th)`);
|
||||
assert.equal(sum(GRAVEL, 'kazimierzShip'), 1 + i, `revive ${i}: <部署时> 卡西米尔 +1`);
|
||||
h.run(0.5);
|
||||
}
|
||||
assert.deepEqual(h.eventsOf('fx').filter((e) => e[1] === 'revive' && e[4] && e[4].left != null).map((e) => e[4].left), [2, 1, 0], 'the band\'s 3');
|
||||
});
|
||||
|
||||
test('埃芒加德: 瑕光\'s <部署时> +4 to her own active bonds fires on a revive, up to its 12 per battle', REAL, () => {
|
||||
const { kill, sum } = setup();
|
||||
assert.equal(sum(BLEMISH, 'kazimierzShip'), 4);
|
||||
kill(BLEMISH);
|
||||
assert.equal(sum(BLEMISH, 'kazimierzShip'), 8, 'first revive');
|
||||
assert.equal(sum(BLEMISH, 'raidShip'), 8);
|
||||
kill(BLEMISH);
|
||||
assert.equal(sum(BLEMISH, 'kazimierzShip'), 12, 'second revive: the cap');
|
||||
kill(BLEMISH);
|
||||
assert.equal(sum(BLEMISH, 'kazimierzShip'), 12, 'third revive: still 12 (max_add_count_per_battle)');
|
||||
});
|
||||
|
||||
test('埃芒加德: a revive resets the operator like a redeploy (SP back to its initial value, a new deployment)', REAL, () => {
|
||||
const { h, kill } = setup();
|
||||
const u = h.unit(BLEMISH);
|
||||
u.skill.gainSp(3, 'test');
|
||||
const seq = u.deploySeq;
|
||||
assert.ok(u.skill.sp > u.skill.initSp + 1e-6, `SP charged (${u.skill.sp})`);
|
||||
kill(BLEMISH);
|
||||
assert.ok(u.deploySeq > seq, 'a new deployment');
|
||||
assert.ok(Math.abs(u.skill.sp - u.skill.initSp) < 1e-6, `SP reset to ${u.skill.initSp}, got ${u.skill.sp}`);
|
||||
assert.equal(Math.round(u.hp), Math.round(u.s.maxHp), 'full HP');
|
||||
});
|
||||
|
||||
test('M3茧甲: the same 复活 — a knock-out (砾 不屈 +2) and a deployment (卡西米尔 +1), once per battle', REAL, () => {
|
||||
const { kill, sum } = setup({ band: null, items: { [GRAVEL]: ['chess_item_4_12_e_a'] } });
|
||||
const u = kill(GRAVEL);
|
||||
assert.ok(u.alive);
|
||||
assert.equal(sum(GRAVEL, 'indomShip'), 2);
|
||||
assert.equal(sum(GRAVEL, 'kazimierzShip'), 2);
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { deploymentOf } from '../../server/sim/content/items/battle.js';
|
||||
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { DUEL_KEY, OATH_KEY, PAIN_KEY, REBORN_KEY } from '../../server/sim/content/kits/ops/op-lessng.js';
|
||||
@@ -327,7 +328,7 @@ test('DRE-Y “苦修者的抗压训练”: the first lethal hit of a deployment
|
||||
done(h);
|
||||
// a running 不死 (坚固维式重锤's window) holds him instead: the module is not used
|
||||
const r = field({ tier, elite: true, mod: DREY });
|
||||
r.u.mem.undyingAt = `${r.u.deploySeq}:0`;
|
||||
r.u.mem.undyingAt = deploymentOf(r.u);
|
||||
r.u.mem.undyingUntil = r.h.b.time + 5;
|
||||
const hammer = r.h.b.on('fatal', (c) => { if (c.unit === r.u && !c.prevented) c.prevented = true; }, { priority: -99 });
|
||||
const f = r.h.spawn('enemy_dummy', { pos: [11, 8] });
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { deploymentOf } from '../../server/sim/content/items/battle.js';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
|
||||
@@ -350,7 +351,7 @@ test('SBL-Y (stages 1 and 3): knocked out ⇒ he stays and heals 30 % max HP, on
|
||||
// a running 不死 (坚固维式重锤's window) keeps him up instead
|
||||
h.b.redeploy(u);
|
||||
const r = field({ tier, elite: true, mod: SBLY, skill: 1 });
|
||||
r.u.mem.undyingAt = `${r.u.deploySeq}:0`;
|
||||
r.u.mem.undyingAt = deploymentOf(r.u);
|
||||
r.u.mem.undyingUntil = r.h.b.time + 5;
|
||||
const hammer = r.h.b.on('fatal', (c) => { if (c.unit === r.u && !c.prevented) c.prevented = true; }, { priority: -99 });
|
||||
const f = r.h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
|
||||
Reference in new issue
Block a user