feedback3: merge WF — 阿戈尔 revives by position, 傀儡师 switches at full HP, 联防 uses the layers the combat reached, deployment is column-major

This commit is contained in:
sganggs committed 2026-10-04 21:05:40 +08:00
commit ee9fc83207
35 files changed
+392 -149

No files matched your search

+10 -8
View File
@@ -13,25 +13,27 @@ const approx = (a, b, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps, `${a} ≈
const guard = (o = {}) => chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 300, blockCnt: 2 }, skill: null, ...o });
const walker = (o = {}) => enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1, ...o });
test('initial deployment order: top→bottom then left→right; right boss side right→left', () => {
test('initial deployment order: by column from the left, top to bottom within a column (PRTS 从上到下>从左到右 as a scan); the right boss side from its own left', () => {
// PRTS 卫戍协议/帮助 §作战阶段 "按从上到下>从左到右的顺序部署" (since 2026-03-14; act 1: "从左到右>从下到上") — down each column,
// the columns from the left: the order PRTS gives the 阿戈尔 devour ("更靠左和靠上"). Until 0.1.3: the top row first
const h = makeBattle({
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }), d: guard({ id: 'd' }) } },
units: [{ chessId: 'a', row: 9, col: 5 }, { chessId: 'b', row: 12, col: 7 }, { chessId: 'c', row: 12, col: 3 }, { chessId: 'd', row: 10, col: 4 }],
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }), d: guard({ id: 'd' }), e: guard({ id: 'e' }) } },
units: [{ chessId: 'a', row: 9, col: 5 }, { chessId: 'b', row: 12, col: 7 }, { chessId: 'c', row: 12, col: 3 }, { chessId: 'd', row: 10, col: 4 }, { chessId: 'e', row: 12, col: 5 }],
content: 'none',
});
h.step();
assert.deepEqual(h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.defId), ['c', 'b', 'd', 'a']);
assert.deepEqual(h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.defId), ['c', 'd', 'e', 'a', 'b']);
assert.equal(h.hooksOf('battleStart').length, 1);
const hb = makeBattle({
kind: 'boss',
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }) } },
players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: [{ uid: 1, chessId: 'a', row: 10, col: 3 }, { uid: 2, chessId: 'b', row: 10, col: 6 }] }],
defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }) } },
players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: [{ uid: 1, chessId: 'a', row: 10, col: 3 }, { uid: 2, chessId: 'b', row: 10, col: 6 }, { uid: 3, chessId: 'c', row: 12, col: 6 }] }],
content: 'none',
});
hb.step();
const order = hb.hooksOf('deploy').filter((c) => c.initial).map((c) => [c.unit.defId, c.unit.tileR, c.unit.tileC, c.unit.facing]);
// board row 10 → boss row 3; mirrored cols: 3 → 17, 6 → 14; right→left in field coords
assert.deepEqual(order, [['a', 3, 17, -1], ['b', 3, 14, -1]]);
// board rows 10 / 12 → boss rows 3 / 5; mirrored cols: 3 → 17, 6 → 14; its own left first = the highest field column
assert.deepEqual(order, [['a', 3, 17, -1], ['c', 5, 14, -1], ['b', 3, 14, -1]]);
});
test('DP: starts at 10, +1/s, cap 99; dead operator redeploys after respawnTime when DP ≥ cost', () => {
+1 -1
View File
@@ -543,7 +543,7 @@ test('ranged enemies attack allies in radius: blocker → taunt → latest deplo
chess: { t_a: guard({ id: 't_a', stats: { maxHp: 1e6, atk: 0 } }), t_b: guard({ id: 't_b', stats: { maxHp: 1e6, atk: 0, tauntLevel: taunt } }) },
enemies: { enemy_caster: enemyRec({ key: 'enemy_caster', hp: 1e6, speed: 1, atk: 100, range: 2.5, dmgType: 'arts' }) },
},
// deploy order: top→bottom, so t_b (row 11) deploys before t_a (row 10) ⇒ t_a is the latest deployed
// deploy order: one column, top to bottom, so t_b (row 11) deploys before t_a (row 10) ⇒ t_a is the latest deployed
units: [{ chessId: 't_a', row: 10, col: 8 }, { chessId: 't_b', row: 11, col: 8 }],
enemies: [{ key: 'enemy_caster', pos: [10, 10] }], content: 'none', autoFinish: false,
});
+30
View File
@@ -158,6 +158,36 @@ test('S1 / S3 running at a lethal hit end with the switch (清除自身一切Buf
assert.equal(s2.h.b.emit('dollSwitch', { unit: s2.g, reason: 'test', done: false }).done, false, 'already the 替身');
});
test('E: a lethal 流失 inside the switch (a second 阿戈尔 devour mark) — she leaves it at full HP, both ways (PRTS "切换途中重设自身生命至最大值"; community report #2)', { skip }, () => {
// 乌尔比安 → 斯卡蒂 → 归溟幽灵鲨 → 深巡 → 幽灵鲨 → 隐现 (5 阿戈尔): 乌尔比安's mark switches her, 斯卡蒂's lands inside
// the 1 s switch (无敌 does not stop a 流失, 不死 holds it at 1 HP; the switch is no knock-out, so the mark is not
// cancelled). Until 0.1.3 she then fought the 替身's 20 s at 1 HP and fell to the first hit
const ids = ['chess_char_5_05_a', 'chess_char_3_05_a', G, 'chess_char_1_04_a', 'chess_char_2_07_a', 'chess_char_1_01_a'];
const h = makeBattle({
seed: 4, timeLimit: 300, autoFinish: false, captureNoisy: true, hooks: ['damaged', 'death'],
units: ids.map((chessId, i) => ({ chessId, row: 10, col: 3 + i })),
bonds: { egirShip: { count: 5, active: true, tier: 2, layers: 0 } },
});
h.step();
const g = h.unit(G);
const marks = h.hooksOf('damaged').filter((c) => c.target === g && c.dmg?.tags?.includes('bond:egir:devour'));
assert.equal(marks.length, 2, 'two devour marks resolve on her');
assert.ok(g.alive && g.trait.doll && g.trait.dollSwitching, 'the first switched her');
assert.ok(g.hp < 2, `the second, inside the switch, left 1 HP (不死; a regen tick aside): ${g.hp}`);
h.run(DOLL_SWITCH + 0.05);
assert.ok(g.trait.doll && !g.trait.dollSwitching, 'the 替身 fights');
approx(g.hp, g.s.maxHp, 'at full HP when the switch ends');
// the switch back: a lethal 流失 inside it is held at 1 HP, then she is the 本体 at full HP
assert.ok(h.runUntil(() => !g.trait.doll && g.trait.dollSwitching, 25), 'switching back');
h.b.loseHp(g, 1e9, { source: null });
assert.ok(g.alive && g.hp < 2, `not knocked out while switching: held at 1 HP (${g.hp})`);
h.run(DOLL_SWITCH + 0.05);
assert.ok(!g.trait.doll && !g.trait.dollSwitching && g.form === null, 'the 本体');
approx(g.hp, g.s.maxHp, 'at full HP when the switch back ends');
assert.equal(h.hooksOf('death').filter((c) => c.unit === g).length, 0, 'never knocked out');
checkInvariants(h.b);
});
test('a 本体 holding 不死 (a running 坚固维式重锤 window) does not switch: held at 1 HP (PRTS "受到足以致命的伤害且未持有不死的情况下")', { skip }, () => {
const { h, g, lethal } = setup({ items: ['chess_item_3_09_e_a'] });
lethal();
+5 -5
View File
@@ -256,16 +256,16 @@ function lastTarget(h, enemy) {
return ev ? h.b.unitById(ev[2]) : null;
}
test('#5 unblocked ranged enemies: highest taunt, then the latest deployed — the bottom row, rightmost (从上到下 > 从左到右)', () => {
const units = [[12, 5], [11, 3], [9, 4], [9, 7], [10, 8]].map(([row, col], i) => ({ chessId: 't_wall', row, col, uid: i + 1 }));
test('#5 unblocked ranged enemies: highest taunt, then the latest deployed — the rightmost column, its bottom (从上到下 > 从左到右: down each column, the columns left to right)', () => {
const units = [[12, 5], [11, 3], [9, 4], [9, 7], [10, 8], [12, 8]].map(([row, col], i) => ({ chessId: 't_wall', row, col, uid: i + 1 }));
const h = arena({ units, enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
h.step();
const order = h.hooksOf('deploy').filter((c) => c.initial).map((c) => [c.unit.tileR, c.unit.tileC]);
assert.deepEqual(order, [[12, 5], [11, 3], [10, 8], [9, 4], [9, 7]]);
assert.deepEqual(order, [[11, 3], [9, 4], [12, 5], [9, 7], [12, 8], [10, 8]]);
h.run(1.2);
const e = h.enemy('enemy_t_shooter');
const t = lastTarget(h, e);
assert.deepEqual([t.tileR, t.tileC], [9, 7], 'the last deployed');
assert.deepEqual([t.tileR, t.tileC], [10, 8], 'the last deployed');
// a taunt level beats deployment order
const h2 = arena({ units, enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
h2.step();
@@ -279,7 +279,7 @@ test('#5 the initial deployment puts every operator before the summons (PRTS 卫
const h = arena({ units: [{ chessId: 't_wall', row: 12, col: 3, uid: 1 }, { chessId: 't_wall', row: 9, col: 8, uid: 2 }, tok], enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
h.step();
const order = h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.uid);
assert.deepEqual(order, [1, 2, 3], 'operators top→bottom, then the summon');
assert.deepEqual(order, [1, 2, 3], 'the operators (by column from the left), then the summon');
const t = h.unit(3);
assert.ok(t.aggroSeq > h.unit(1).aggroSeq && t.aggroSeq > h.unit(2).aggroSeq);
h.run(1.2);
+4
View File
@@ -175,6 +175,10 @@ test('dollkeeper: fatal damage ⇒ a 1 s switch, 20 s as the substitute (block 0
assert.equal(h.b.applyStatus(u, 'stun', { duration: 5 }), false, '眩晕 immune');
h.b.loseHp(u, 1e5);
assert.ok(u.alive && u.trait.doll && u.hp >= 1, '不死');
const low = u.hp;
assert.ok(u.s.flags.noHeal && u.s.flags.healFree, '禁疗 during the switch');
assert.equal(h.b.heal(u, u, 500), 0, 'a self-heal does not land');
approx(u.hp, low);
u.hp = 2000;
h.run(1.05);
assert.ok(u.trait.doll && !u.trait.dollSwitching, 'fighting as the substitute');