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:
commit
ee9fc83207
35 files changed
+392
-149
No files matched your search
+114
-30
@@ -374,37 +374,43 @@ test('阿戈尔 devour order: left first, then top first (row 0 is the bottom: t
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 5: the first 3 members knocked out for the first time redeploy at once (death + deploy fire); a second knock-out is final', () => {
|
||||
test('阿戈尔 5: the first 3 members by position (left column first, top first) revive at once on their first knock-out, whatever the order of knock-outs; the others never; a second knock-out is final', () => {
|
||||
const list = [];
|
||||
for (let i = 0; i < 5; i++) list.push([`g${i}_a`, ['egirShip'], { skill: { spCost: 30, initSp: 5, duration: 10 } }]);
|
||||
// one per row pair so nobody devours anybody
|
||||
// one per row pair so nobody devours anybody: col 3 rows 9–12 (g0 bottom … g3 top), g4 at (9,5). PRTS 盟约记录 阿戈尔
|
||||
// "前3名【阿戈尔】干员首次被击倒时立刻复活", the first 3 being the first deployed — "更靠左和靠上": g3, g2, g1
|
||||
const units = list.map(([id], i) => ({ chessId: id, row: 9 + (i % 4), col: 3 + 2 * Math.floor(i / 4) }));
|
||||
const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) }, hooks: ['death', 'deploy'] });
|
||||
h.step(1);
|
||||
const us = list.map(([id]) => h.unit(id));
|
||||
const kill = (u) => h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
us[0].skill.gainSp(20, 'test');
|
||||
const tiles = us.map((u) => [u.tileR, u.tileC]);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const redeploys = () => h.hooksOf('deploy').filter((c) => c.unit === us[i] && !c.initial).length;
|
||||
const before = redeploys();
|
||||
kill(us[i]);
|
||||
assert.ok(us[i].alive && us[i].deployed, `member ${i} back on the field`);
|
||||
close(us[i].hp, us[i].s.maxHp);
|
||||
assert.deepEqual([us[i].tileR, us[i].tileC], tiles[i], 'same tile');
|
||||
assert.ok(h.hooksOf('death').some((c) => c.unit === us[i] && c.reason === 'killed'), 'it was knocked out (被击倒 triggers fire)');
|
||||
assert.equal(redeploys(), before + 1, `member ${i} redeployed`);
|
||||
const redeploys = (u) => h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length;
|
||||
// knocked out first, yet no beneficiary: g0 (bottom of the first column) and g4 (second column) stay down
|
||||
for (const u of [us[0], us[4]]) {
|
||||
kill(u);
|
||||
assert.ok(!u.alive, `${u.defId} is not among the first 3 by position: no revive`);
|
||||
assert.equal(redeploys(u), 0);
|
||||
}
|
||||
assert.equal(us[0].skill.sp, 5, 'a redeploy restarts from the initial SP');
|
||||
kill(us[3]);
|
||||
assert.ok(!us[3].alive, 'the 4th dies');
|
||||
kill(us[0]);
|
||||
assert.ok(!us[0].alive, 'second knock-out is final');
|
||||
us[1].skill.gainSp(20, 'test');
|
||||
const tiles = us.map((u) => [u.tileR, u.tileC]);
|
||||
for (const i of [1, 3, 2]) {
|
||||
const u = us[i];
|
||||
const before = redeploys(u);
|
||||
kill(u);
|
||||
assert.ok(u.alive && u.deployed, `member ${i} back on the field`);
|
||||
close(u.hp, u.s.maxHp);
|
||||
assert.deepEqual([u.tileR, u.tileC], tiles[i], 'same tile');
|
||||
assert.ok(h.hooksOf('death').some((c) => c.unit === u && c.reason === 'killed'), 'it was knocked out (被击倒 triggers fire)');
|
||||
assert.equal(redeploys(u), before + 1, `member ${i} redeployed`);
|
||||
}
|
||||
assert.equal(us[1].skill.sp, 5, 'a redeploy restarts from the initial SP');
|
||||
kill(us[1]);
|
||||
assert.ok(!us[1].alive, 'second knock-out is final');
|
||||
checkInvariants(h.b);
|
||||
// a member moved off its board tile (突袭 / relocation) revives where it was knocked out; later redeploys go home
|
||||
const hm = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) }, hooks: [] });
|
||||
hm.step(1);
|
||||
const m = hm.unit('g4_a');
|
||||
const m = hm.unit('g3_a');
|
||||
const home = [m.homeR, m.homeC];
|
||||
assert.ok(hm.b.relocate(m, 12, 8));
|
||||
hm.b.dealDamage(null, m, { amount: 1e9, type: 'true' });
|
||||
@@ -412,16 +418,17 @@ test('阿戈尔 5: the first 3 members knocked out for the first time redeploy a
|
||||
assert.deepEqual([m.homeR, m.homeC], home, 'home unchanged');
|
||||
checkInvariants(hm.b);
|
||||
// 3 members: no revive
|
||||
const h3 = makeBattle({ defs: defsOf(list), units: units.slice(0, 3), bonds: { egirShip: bondOn(3, 0, null, [3, 5]) } });
|
||||
const h3 = makeBattle({ defs: defsOf(list), units: units.slice(1, 4), bonds: { egirShip: bondOn(3, 0, null, [3, 5]) } });
|
||||
h3.step(1);
|
||||
h3.b.dealDamage(null, h3.unit('g0_a'), { amount: 1e9, type: 'true' });
|
||||
assert.ok(!h3.unit('g0_a').alive);
|
||||
h3.b.dealDamage(null, h3.unit('g3_a'), { amount: 1e9, type: 'true' });
|
||||
assert.ok(!h3.unit('g3_a').alive);
|
||||
});
|
||||
|
||||
test('阿戈尔 5 + the devour chain (GitHub #33): a member the devour knocks out revives, its pending marks are cancelled and it stays standing (normal and 联防)', () => {
|
||||
test('阿戈尔 5 + the devour chain (GitHub #33): the devour knocks out the food, only the members among the first 3 by position come back; the others keep theirs for the fight (normal and 联防)', () => {
|
||||
// one row, all facing right: 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现. PRTS 盟约记录 阿戈尔 备注 "标记按付与顺序触发
|
||||
// 【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; until 0.1.1 the marks still pending on a revived member
|
||||
// knocked it out again, so all 3 revives went at t = 0 and the members stayed down
|
||||
// 【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果". The 3 revives belong to the first 3 by position
|
||||
// (浊心斯卡蒂, 乌尔比安, 幽灵鲨 — community reports 「阿戈尔盟约复活有bug,只能复活一个干员」); until 0.1.3 the first 3
|
||||
// knocked out took them, so the food (幽灵鲨, 海霓, 深巡) spent all 3 at t = 0 and nobody came back in the fight
|
||||
const ids = ['chess_char_6_04_a', 'chess_char_5_05_a', 'chess_char_2_07_a', 'chess_char_3_09_a', 'chess_char_1_04_a', 'chess_char_1_01_a'];
|
||||
for (const kind of ['normal', 'unite']) {
|
||||
const h = makeBattle({
|
||||
@@ -434,16 +441,93 @@ test('阿戈尔 5 + the devour chain (GitHub #33): a member the devour knocks ou
|
||||
// comes from one (幽灵鲨's, 海霓's, 深巡's)
|
||||
assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]),
|
||||
[ulpia, ghost, hn, deep, fodder].map((t) => [skadi.defId, t.defId]), kind);
|
||||
const revived = (u) => h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length;
|
||||
for (const u of [ghost, hn, deep]) {
|
||||
assert.equal(h.hooksOf('death').filter((c) => c.unit === u && c.reason === 'killed').length, 1, `${kind}: ${u.defId} knocked out once`);
|
||||
assert.equal(h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length, 1, `${kind}: ${u.defId} revived at once`);
|
||||
}
|
||||
assert.equal(revived(ghost), 1, `${kind}: 幽灵鲨 (third by position) revived at once`);
|
||||
assert.equal(revived(hn) + revived(deep), 0, `${kind}: 海霓 and 深巡 (fourth, fifth) not revived`);
|
||||
h.run(3);
|
||||
for (const u of [skadi, ulpia, ghost, hn, deep]) assert.ok(u.alive && u.deployed && u.hp > 0, `${kind}: ${u.defId} standing`);
|
||||
assert.ok(!fodder.alive, `${kind}: the fodder (no member) stays down`);
|
||||
// the 3 revives went to the first knock-outs at t = 0, as officially: the next knock-out of a member is final
|
||||
for (const u of [skadi, ulpia, ghost]) assert.ok(u.alive && u.deployed && u.hp > 0, `${kind}: ${u.defId} standing`);
|
||||
for (const u of [hn, deep, fodder]) assert.ok(!u.alive, `${kind}: ${u.defId} down`);
|
||||
// 浊心斯卡蒂 and 乌尔比安 still hold theirs: their first knock-out in the fight is revived, 幽灵鲨's second is final
|
||||
for (const u of [skadi, ulpia]) {
|
||||
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
assert.ok(u.alive && revived(u) === 1, `${kind}: ${u.defId} revived in the fight`);
|
||||
}
|
||||
h.b.dealDamage(null, ghost, { amount: 1e9, type: 'true' });
|
||||
assert.ok(!ghost.alive, `${kind}: no revive left`);
|
||||
assert.ok(!ghost.alive, `${kind}: 幽灵鲨 has no revive left`);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
test('阿戈尔 5 beneficiaries: fixed at battle start — a 联防 member entering down keeps the slot, a 调和 member is one, the mirrored boss side counts from its own left', () => {
|
||||
const list = [['g0_a', ['egirShip']], ['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']], ['g4_a', ['egirShip']], ['mani_a', ['maniShip']]];
|
||||
// five members in five columns (one per column, nobody in front of anybody): g0 col 3 … g4 col 7
|
||||
const units = list.slice(0, 5).map(([chessId], i) => ({ chessId, row: 9 + (i % 2) * 3, col: 3 + i }));
|
||||
const revives = (h, id) => {
|
||||
const u = h.unit(id);
|
||||
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
return u.alive;
|
||||
};
|
||||
// 联防: g0 entered down (forced out before battleStart). The slots stay the first 3 by position — g0, g1, g2.
|
||||
// The forced exit is not a 击倒, so g0's charge waits until they stand back up. g3 is not promoted into it.
|
||||
const hu = makeBattle({
|
||||
kind: 'unite', defs: defsOf(list), autoFinish: false, timeLimit: 60, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) },
|
||||
units: units.map((u, i) => (i === 0 ? { ...u, carryState: { down: true } } : u)),
|
||||
});
|
||||
hu.step(1);
|
||||
const g0 = hu.unit('g0_a');
|
||||
assert.ok(!g0.alive, 'g0 forced out');
|
||||
assert.deepEqual(['g4_a', 'g3_a', 'g1_a', 'g2_a'].map((id) => revives(hu, id)), [false, false, true, true]);
|
||||
assert.ok(hu.runUntil(() => g0.alive && g0.deployed, 25), 'g0 stands back up inside the redeploy time');
|
||||
assert.equal(revives(hu, 'g0_a'), true, 'g0 still held the first slot');
|
||||
hu.b.dealDamage(null, g0, { amount: 1e9, type: 'true' });
|
||||
assert.ok(!g0.alive, 'g0 has no second revive');
|
||||
checkInvariants(hu.b);
|
||||
// 调和 (an active 调和 member enjoys the core bonds) in the first column: one of the 3
|
||||
const hm = makeBattle({
|
||||
defs: defsOf(list), autoFinish: false, timeLimit: 60,
|
||||
units: [{ chessId: 'mani_a', row: 11, col: 2 }, ...units.slice(0, 4)],
|
||||
bonds: { egirShip: bondOn(5, 0, null, [3, 5]), maniShip: { count: 1, active: true, tier: 1, layers: 0 } },
|
||||
});
|
||||
hm.step(1);
|
||||
assert.deepEqual(['g2_a', 'mani_a', 'g0_a', 'g1_a'].map((id) => revives(hm, id)), [false, true, true, true]);
|
||||
// the right-hand boss player (mirrored): its board's left is the field's right — board cols 3, 4, 5 = field cols 17,
|
||||
// 16, 15 hold the revives (read in field columns from the left they would go to g4, g3, g2)
|
||||
const hb = makeBattle({
|
||||
kind: 'boss', defs: defsOf(list), autoFinish: false, timeLimit: 60,
|
||||
players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: units.map((u, i) => ({ ...u, uid: i + 1 })), bonds: { egirShip: bondOn(5, 0, null, [3, 5]) } }],
|
||||
});
|
||||
hb.step(1);
|
||||
assert.deepEqual(['g4_a', 'g3_a', 'g0_a', 'g1_a', 'g2_a'].map((id) => [hb.unit(id).tileC, revives(hb, id)]),
|
||||
[[13, false], [14, false], [17, true], [16, true], [15, true]]);
|
||||
checkInvariants(hb.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 5: a beneficiary 埃芒加德 or M3茧甲 save in place has used its 阿戈尔 revive [ASSUMED]; the others keep theirs', () => {
|
||||
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 kill = (h, id) => { const u = h.unit(id); h.b.dealDamage(null, u, { amount: 1e9, type: 'true' }); return u; };
|
||||
for (const how of ['band', 'item']) {
|
||||
const h = makeBattle({
|
||||
defs: defsOf(list), autoFinish: false, timeLimit: 60, hooks: ['death', 'deploy'],
|
||||
units: units(how === 'item' ? { 0: ['chess_item_4_12_e_a'] } : {}), ...(how === 'band' ? { bandId: 'band_ermengard' } : {}),
|
||||
bonds: { egirShip: bondOn(5, 0, null, [3, 5]) },
|
||||
});
|
||||
h.step(1);
|
||||
// g0, g1, g2 (columns 3–5) hold the 阿戈尔 revives; g0 is saved in place first
|
||||
const g0 = kill(h, 'g0_a');
|
||||
assert.ok(g0.alive && h.hooksOf('death').filter((c) => c.unit === g0).length === 0, `${how}: saved in place (no knock-out)`);
|
||||
// 埃芒加德 saves the battle's first 3 knock-downs: its other 2 go to g3 and g4 (no 阿戈尔 beneficiaries)
|
||||
if (how === 'band') for (const id of ['g3_a', 'g4_a']) assert.ok(kill(h, id).alive, 'band revive');
|
||||
kill(h, 'g0_a');
|
||||
assert.ok(!g0.alive, `${how}: its 阿戈尔 revive went with the save — its first knock-out is final`);
|
||||
assert.equal(h.hooksOf('deploy').filter((c) => c.unit === g0 && !c.initial).length, 0);
|
||||
for (const id of ['g1_a', 'g2_a']) {
|
||||
const u = kill(h, id);
|
||||
assert.ok(u.alive && h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length === 1, `${how}: ${id} revived by 阿戈尔`);
|
||||
}
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -321,7 +321,8 @@ for (const key of ['enemy_1203_sfhu', 'enemy_1203_sfhu_2']) {
|
||||
// 持续时间和效果独立计算"; "普通攻击对目标对及目标周围半径1.0范围内的所有我方单位造成法术普通伤害" (0.1.3: the blast took the attack
|
||||
// radius 2 and left a steam zone; the attack hit its target only)
|
||||
test(`${nm(key)}: death spawns 4 茶器; the blast (r 1.25, ATK arts) gives each unit hit one ${tb(key, 'DeadBoom.duration')} s layer of ASPD ${tb(key, 'DeadBoom.attack_speed')}; its attacks splash r 1.0`, () => {
|
||||
const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }, { chessId: 't_wall', row: 11, col: 8 }, { chessId: 't_wall2', row: 10, col: 5 }], hooks: ['damaged'], captureNoisy: true });
|
||||
// t_gun deploys last (column 6, below t_wall): the attack's target, t_wall2 and t_wall within 1.0 of it
|
||||
const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }, { chessId: 't_wall', row: 11, col: 6 }, { chessId: 't_wall2', row: 10, col: 5 }], hooks: ['damaged'], captureNoisy: true });
|
||||
h.step();
|
||||
const e = put(h, key, [10, 7]);
|
||||
h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === e && c.dmg.isAttack), 10);
|
||||
@@ -1372,7 +1373,8 @@ test(`${nm('enemy_1050_lslime')}: 4 targets, burning DoT on hit, ASPD up below h
|
||||
// 目标造成物理普通伤害,对溅射目标造成物理溅射伤害") — community report #14: until 0.1.3 the target took a full attack and the 4
|
||||
// orthogonal neighbours 100 % ATK (no diagonal), DEF down for 3 s
|
||||
test(`${nm('enemy_1500_skulsr')}: an unblocked attack is a grenade — 26 % ATK on the target and on its 8 surrounding tiles, DEF −50 % for 5 s; blocked a plain hit; ATK up below half`, () => {
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 11, col: 3 }, { chessId: 't_wall2', row: 12, col: 6 }, { chessId: 't_wall3', row: 11, col: 5 }], captureNoisy: true, hooks: ['statusApplied', 'damaged'] });
|
||||
// t_wall3 is the one in range (√5): the target; t_wall2 on its diagonal, t_wall two tiles away
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 11, col: 3 }, { chessId: 't_wall2', row: 12, col: 4 }, { chessId: 't_wall3', row: 11, col: 5 }], captureNoisy: true, hooks: ['statusApplied', 'damaged'] });
|
||||
h.step();
|
||||
const e = put(h, 'enemy_1500_skulsr', [10, 7]);
|
||||
h.runUntil(() => e.stats.attacks >= 1, 10);
|
||||
|
||||
@@ -298,6 +298,10 @@ test('余: S3 fire wall adds burn to allied arts damage crossing it and clears c
|
||||
enemies: [{ key: 'enemy_dummy', pos: [10, 6] }, { key: 'enemy_shooter', pos: [11, 8] }],
|
||||
seed: 11,
|
||||
});
|
||||
// the shooter aims at 优等生 behind the wall: a taunt, since 余 (his column further right) now deploys last — the
|
||||
// latest deployed, the shooter's pick at equal taunt (deployment by column since 0.1.3)
|
||||
h.step();
|
||||
h.b.addBuff(h.unit('chess_char_1_15_a'), { key: 'test:taunt', mods: { taunt: 1 }, persist: true });
|
||||
const u = h.unit('chess_char_6_03_a');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 10));
|
||||
h.run(15);
|
||||
|
||||
@@ -99,6 +99,19 @@ test('联防 helpers follow unite.helperOrder (units > active bond > standing >
|
||||
const c = player('c', 2, 2, 5, true); // most units + an active bond
|
||||
const order = helperOrder(m, [a, b, c], new Map()).map((p) => p.playerId);
|
||||
assert.deepEqual(order, ['c', 'b'], 'LP never decides; the first helper (right-hand field) is c');
|
||||
// the tie-break is the layers the 联防 will fight with: stored plus this round's pending gains, capped at 999
|
||||
const reached = (playerId, seat, stored, pending) => ({
|
||||
playerId, seat, lp: 10, deployCount: 4, board: new Map(),
|
||||
layers: { bond_x: stored },
|
||||
pendingLayerGains: pending,
|
||||
bonds: { bond_x: { active: true, layers: stored } },
|
||||
});
|
||||
const hi = reached('A', 0, 200, { bond_x: 50 }); // fights at 250
|
||||
const lo = reached('B', 1, 230, null); // fights at 230
|
||||
assert.deepEqual(helperOrder(m, [lo, hi], new Map()).map((p) => p.playerId), ['A', 'B']);
|
||||
const capped = reached('C', 2, 990, { bond_x: 50 }); // 999, not 1040
|
||||
const under = reached('D', 3, 995, null);
|
||||
assert.deepEqual(helperOrder(m, [under, capped], new Map()).map((p) => p.playerId), ['C', 'D']);
|
||||
assert.ok(!/highest LP, then seat/.test(DESIGN), 'DESIGN §6.1 no longer picks helpers by LP');
|
||||
assert.match(DESIGN, /helperOrder/);
|
||||
assert.match(META, /unite\.js helperOrder/);
|
||||
@@ -515,7 +528,8 @@ test('user playtest #6 follow-up: a merge consuming a deployed copy puts the eli
|
||||
assert.equal(promo.eliteTileAmongSeveralDeployed?.assumed, true);
|
||||
for (const [name, text] of [['research 01', R01], ['00-INDEX', INDEX], ['META', META], ['SIM', SIM], ['PlayerState', PS]]) assert.match(text.replace(/\s+|\/\/\s/g, ''), official, `${name} quotes PRTS`);
|
||||
assert.match(INDEX, /to \*\*that copy's board position\*\*/);
|
||||
assert.match(META, /of several, the one that deploys first \(row desc, then col asc; `board\.js mergeTile`/);
|
||||
// the deployment order is by column since 0.1.3 (Battle.start; DESIGN §23.f4), so is the merge's tile among several copies
|
||||
assert.match(META, /of several, the one that deploys first \(col asc, then row desc; `board\.js mergeTile`/);
|
||||
assert.ok(!/it takes a freed\s+board tile of a consumed copy only when the hand and temp are both full/.test(META), 'META: the old fallback-only wording is gone');
|
||||
assert.ok(!/only with the hand and temp both full does it take a/.test(PS), 'PlayerState header: the old fallback-only wording is gone');
|
||||
assert.match(PLAYING, /精锐会直接出现在\*\*那名干员的位置\*\*/);
|
||||
|
||||
@@ -88,7 +88,7 @@ test('merge with a full hand and a full temp: the elite takes its deployed copy\
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true }, 'a purchase that completes a merge is allowed with a full hand');
|
||||
const elite = [...ps.board.values()].find((p) => p.id === m.gd.goldenIdOf(id));
|
||||
assert.ok(elite, 'the elite stands on a consumed copy\'s tile');
|
||||
assert.equal(ps.find(elite.uid).key, `${t1[0]},${t1[1]}`, 'the copy that deploys first (legalTileFor walks the deploy order)');
|
||||
assert.equal(ps.find(elite.uid).key, `${t1[0]},${t1[1]}`, 'the copy that deploys first (legalTileFor walks the top row left to right: on one row, the deploy order)');
|
||||
assert.equal(elite.poolCopies, 3, 'it holds all three copies');
|
||||
assert.deepEqual(elite.items.map((x) => x.id).sort(), ['chess_item_1_01_e_a', 'chess_item_2_04_e_a'], 'returned items are kept on it');
|
||||
checkInvariants(m);
|
||||
|
||||
@@ -116,7 +116,8 @@ test('merge from two board copies with a full hand: the elite takes the copy tha
|
||||
stock(m, ps, id);
|
||||
assert.equal(ps.deployCount, 2);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true });
|
||||
// deploy order = row desc, then col asc: (9,3) before (9,4) — [ASSUMED] the copy the battle deploys first
|
||||
// deploy order = by column from the left (col asc, then row desc; Battle.start): (9,3) before (9,4) — [ASSUMED] the copy
|
||||
// the battle deploys first
|
||||
const elite = ps.board.get('9,3');
|
||||
assert.ok(elite && elite.id === DATA.chess[id].goldenId, 'the elite stands on (9,3)');
|
||||
assert.equal(elite.dir, 'DOWN', 'with the facing of the copy that stood there');
|
||||
@@ -358,10 +359,10 @@ test('a merge completed during SETTLE (突变细胞) keeps its reward offer for
|
||||
if (holderTile) assert.ok(!ps.board.has(holderTile), `${label}: the carrier's tile is empty`);
|
||||
if (deployedTiles.length) {
|
||||
// PRTS 卫戍协议/帮助: a merge consuming a deployed copy sends the elite to that copy's tile — outside PREP too; of
|
||||
// several, the first in deploy order (row desc, col asc)
|
||||
// several, the first in deploy order (col asc, then row desc — by column from the left, Battle.start)
|
||||
const map = ps.deployMap();
|
||||
const pos = positionClass(m.gd.chess(elite.id));
|
||||
const legal = deployedTiles.map((k) => k.split(',').map(Number)).filter(([r, c]) => canPlace(map, pos, r, c)).sort((a, b) => b[0] - a[0] || a[1] - b[1]);
|
||||
const legal = deployedTiles.map((k) => k.split(',').map(Number)).filter(([r, c]) => canPlace(map, pos, r, c)).sort((a, b) => a[1] - b[1] || b[0] - a[0]);
|
||||
assert.ok(legal.length, `${label}: a legal deployed tile exists`);
|
||||
assert.equal(loc.area, 'board', `${label}: the elite stands on the board`);
|
||||
assert.equal(loc.key, `${legal[0][0]},${legal[0][1]}`, `${label}: on the first deployed tile in deploy order`);
|
||||
|
||||
@@ -86,11 +86,12 @@ test('buy: one deployed copy + one hand copy → the elite takes the deployed co
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('buy: two deployed copies → the copy that deploys first (row desc, then col asc) gives its tile and facing, in either placement order', () => {
|
||||
test('buy: two deployed copies → the copy that deploys first (col asc, then row desc: by column from the left) gives its tile and facing, in either placement order', () => {
|
||||
for (const order of ['first-placed-first', 'first-placed-last']) {
|
||||
const { m, ps } = prep({ seed: 42 });
|
||||
const [id] = meleeTier1(m);
|
||||
// two legal tiles, the first of them in deploy order
|
||||
// two legal tiles, the first of them in deploy order (legalTileFor walks the top row left to right: on one row, the
|
||||
// deploy order)
|
||||
const t1 = legalTileFor(m, ps, id);
|
||||
const t2 = legalTileFor(m, ps, id, new Set([tileKey(...t1)]));
|
||||
const [early, late] = order === 'first-placed-first' ? [t1, t2] : [t2, t1];
|
||||
@@ -101,7 +102,7 @@ test('buy: two deployed copies → the copy that deploys first (row desc, then c
|
||||
stock(m, ps, id);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true });
|
||||
const want = mergeTile([t1, t2].map((t) => ({ key: tileKey(...t) })));
|
||||
assert.equal(want.key, tileKey(...t1), 'legalTileFor walks the deploy order');
|
||||
assert.equal(want.key, tileKey(...t1), 'the first in deploy order');
|
||||
const elite = ps.board.get(want.key);
|
||||
assert.ok(elite && m.gd.isGolden(elite.id), `${order}: the elite on ${want.key}`);
|
||||
assert.equal(elite.dir, want.key === tileKey(...early) ? 'UP' : 'LEFT', `${order}: the facing of the copy that stood there`);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createStore, initialState } from '../../public/js/store.js';
|
||||
import * as specMod from '../../server/sim/spec.js';
|
||||
import { DataSource } from '../../server/sim/simdata.js';
|
||||
import { DATA, makeMatch, checkInvariants } from './harness.js';
|
||||
import { FakeBattle } from './fakeBattle.js';
|
||||
|
||||
const row = (h, pid) => h.m.publicView().players.find((p) => p.playerId === pid);
|
||||
const entry = (list, id) => (list || []).find((b) => b.bondId === id) || null;
|
||||
@@ -122,6 +123,36 @@ test('the 联防 phase keeps showing the gains (a teammate watching the helper s
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('the 联防 field fights with the layers the helper\'s own combat reached (PRTS 联防阶段 "以其阵地当前的状态"), capped like the settlement, which still adds them once', () => {
|
||||
// p_0 perfect with gains, p_1 leaks: 联防 with p_0 as the helper. Until 0.1.3 the 联防 input carried the round-start
|
||||
// layers while the strip showed the reached ones (community report #24: 不屈 at 200+ layers "sometimes not triggering")
|
||||
const h = makeMatch({
|
||||
mode: 'coop', humans: 2, seed: 95, fake: true,
|
||||
script: (b) => (b.round === 1 && b.kind === 'normal' ? { layerGains: { p_0: { indomShip: 20, yanShip: 9 } }, leaks: { p_1: 2 } } : {}),
|
||||
}).start();
|
||||
const m = h.m;
|
||||
h.toPrep(1);
|
||||
const a = h.ps('p_0');
|
||||
a.layers.indomShip = 230;
|
||||
a.layers.yanShip = 995;
|
||||
a.recompute();
|
||||
const first = FakeBattle.instances.length;
|
||||
const input = (kind, round) => FakeBattle.instances.slice(first).find((x) => x.kind === kind && x.round === round)?.opts.players.find((p) => p.playerId === 'p_0');
|
||||
h.drive(() => m.phase === PHASE.UNITE || m.phase === PHASE.SETTLE);
|
||||
assert.equal(m.phase, PHASE.UNITE, 'p_1 leaked, p_0 was perfect: 联防');
|
||||
assert.deepEqual([input('normal', 1).bonds.indomShip.layers, input('normal', 1).bonds.yanShip.layers], [230, 995], 'the own combat: the round-start layers');
|
||||
const u = input('unite', 1);
|
||||
assert.equal(u.bonds.indomShip.layers, 250, '联防: 230 + the 20 reached in the own combat');
|
||||
assert.equal(u.bonds.yanShip.layers, BOND_LAYER_CAP, '995 + 9 is capped at 999, as the settlement caps it');
|
||||
assert.equal(u.bonds.indomShip.layers, entry(a.privateView().bonds, 'indomShip').layers, 'the count the strip shows');
|
||||
assert.deepEqual([a.layers.indomShip, a.bonds.indomShip.layers], [230, 230], 'nothing persistent yet');
|
||||
h.drive(() => m.phase === PHASE.COMBAT && m.round === 2);
|
||||
assert.deepEqual([a.layers.indomShip, a.layers.yanShip], [250, BOND_LAYER_CAP], 'settled once (not 270)');
|
||||
assert.equal(input('normal', 2).bonds.indomShip.layers, 250, 'the next combat starts from 250');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
// ---- the browser runner's live layers ---------------------------------------------------------------------------------
|
||||
|
||||
const DS = new DataSource(DATA, null);
|
||||
|
||||
+10
-8
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -56,11 +56,13 @@ describe('gameLogic.mergeTarget (mirror of server board.js mergeTile)', () => {
|
||||
assert.deepEqual(mergeTarget({ board: [onBoard(A, 10, 4, 'UP')], hand: [inHand(A, 2)] }, A, getChess), { row: 10, col: 4, dir: 'UP' });
|
||||
assert.deepEqual(mergeTarget({ board: [onBoard(A, 10, 4)], temp: [inHand(A, 2)] }, A, getChess), { row: 10, col: 4, dir: 'RIGHT' }, 'temp copies count');
|
||||
});
|
||||
test('two deployed copies → the one that deploys first: row desc, then col asc', () => {
|
||||
test('two deployed copies → the one that deploys first: col asc, then row desc (by column from the left, Battle.start)', () => {
|
||||
const priv = { board: [onBoard(A, 9, 2, 'DOWN'), onBoard(A, 11, 7, 'LEFT')], hand: [] };
|
||||
assert.deepEqual(mergeTarget(priv, A, getChess), { row: 11, col: 7, dir: 'LEFT' });
|
||||
assert.deepEqual(mergeTarget(priv, A, getChess), { row: 9, col: 2, dir: 'DOWN' }, 'the left column, though lower (row-major until 0.1.3)');
|
||||
const same = { board: [onBoard(A, 10, 8, 'UP'), onBoard(A, 10, 3, 'DOWN')], hand: [] };
|
||||
assert.deepEqual(mergeTarget(same, A, getChess), { row: 10, col: 3, dir: 'DOWN' });
|
||||
const column = { board: [onBoard(A, 9, 4, 'UP'), onBoard(A, 12, 4, 'LEFT')], hand: [] };
|
||||
assert.deepEqual(mergeTarget(column, A, getChess), { row: 12, col: 4, dir: 'LEFT' }, 'one column: the top first');
|
||||
});
|
||||
test('elites and other operators are ignored; 风丸 merges with one deployed copy', () => {
|
||||
const priv = { board: [{ ...onBoard(golden(A), 12, 2), golden: true }, onBoard('chess_char_1_02_a', 12, 3), onBoard(A, 9, 9)], hand: [inHand(A, 5)] };
|
||||
|
||||
Reference in new issue
Block a user