- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
81 lines
5.4 KiB
JavaScript
81 lines
5.4 KiB
JavaScript
// Community report of 2026-10-06, item 55: 「死亡后观战视角看不到场上的关底boss」. Reproduced in the browser (an eliminated
|
||
// guest at the boss round, notes i55-repro): in the Final Assault fight the watched boss field shows the leader, but in
|
||
// the boss round's prep the eliminated player saw its own empty normal board — and a scout of a teammate (m.field
|
||
// prep) was the normal board too, while the players themselves prepare on their half of the boss field with the leader
|
||
// standing at its spawn (render/app.js setLeader, only on the boss-field prep camera). Now the scout of a boss round's
|
||
// prep is built on the boss field: the scouted player's pieces on its half (bossFieldPlacement: rows − 7, the right
|
||
// half mirrored, RIGHT ↔ LEFT), `side` for the camera, the leader in nextEnemies with its spawn tile; with item 56 an
|
||
// eliminated viewer is pushed that scout at the round start without a tap.
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { GEO, PHASE } from '../../shared/constants.js';
|
||
import { makeMatch, give, legalTileFor, chessOfTier } from './harness.js';
|
||
import { bossFieldPlacement } from '../../server/match/finalAssault.js';
|
||
import { viewKind, leaderShown } from '../../public/js/render/app/view.js';
|
||
import { leaderStand } from '../../public/js/render/prepfield.js';
|
||
|
||
function bossPrep(seed) {
|
||
const h = makeMatch({ mode: 'coop', difficulty: 'FUNNY', humans: 4, seed, fake: true }).start();
|
||
const m = h.m;
|
||
m.bossPairs = [['p_0', 'p_1'], ['p_2', 'p_3']]; // explicit mirrored-layout fixture, not the pairing policy
|
||
h.drive(() => m.phase === PHASE.PREP && m.round === m.gd.bossRound, { ready: true });
|
||
return h;
|
||
}
|
||
|
||
test('a scout of a boss round\'s prep is the boss field: the player\'s half (mirrored on the right), its side, the leader\'s spawn', () => {
|
||
const h = bossPrep(55);
|
||
const m = h.m;
|
||
assert.deepEqual(m.bossWaves.map((w) => w.players), [['p_0', 'p_1'], ['p_2', 'p_3']]);
|
||
const p1 = h.ps('p_1');
|
||
// a board piece facing RIGHT and one in the hand
|
||
for (const k of [...p1.board.keys()]) p1.board.delete(k);
|
||
const id = chessOfTier(1)[0];
|
||
const tile = legalTileFor(m, p1, id);
|
||
const onBoard = give(m, p1, id, 'board', tile);
|
||
onBoard.dir = 'RIGHT';
|
||
give(m, p1, chessOfTier(1)[1], 'hand', 2);
|
||
h.ps('p_3').eliminate(13);
|
||
assert.deepEqual(m.handle('p_3', { t: 'g.watch', fieldId: 'n:p_1' }), { ok: true });
|
||
const meta = h.lastTo('p_3', 'm.field');
|
||
assert.deepEqual([meta.fieldId, meta.prep, meta.kind, meta.side], ['n:p_1', true, 'boss', 'R']);
|
||
assert.deepEqual(meta.rect, GEO.BOSS_RECT);
|
||
const u = meta.units.find((x) => x.uid === onBoard.uid);
|
||
const want = bossFieldPlacement('R', tile[0], tile[1], 'RIGHT');
|
||
assert.deepEqual([u.y, u.x, u.dir, u.facing], [want.row, want.col, 'LEFT', -1], 'mirrored onto the right half, still facing the leader');
|
||
const bench = meta.units.find((x) => x.y === 0);
|
||
assert.ok(bench && bench.x === 20 - 2, `the hand row on the half's bench row (boss row 0, mirrored): ${JSON.stringify(bench)}`);
|
||
for (const x of meta.units.filter((x) => x.ownerId === 'p_1')) assert.ok(x.y >= 0 && x.y <= 5 && x.x >= 10 && x.x <= 20, `on the right half: ${x.y},${x.x}`);
|
||
// the leader stands at its spawn tile: the client's leaderStand finds it in the scout's nextEnemies
|
||
const stand = leaderStand(meta.nextEnemies, () => null, (x, y) => [[y, x]]);
|
||
assert.ok(stand && stand.entry.boss && stand.row >= 0 && stand.row <= 5, `the leader with its spawn tile: ${JSON.stringify(stand?.entry)}`);
|
||
assert.deepEqual(meta.nextEnemies, m.nextEnemiesFor(p1), 'the scouted player\'s own preview (its pair\'s wave)');
|
||
// the client frames a scout with that rect on the boss-field prep camera, which shows the leader
|
||
assert.equal(viewKind('prep', { rect: meta.rect, side: meta.side }), 'bossPrep');
|
||
assert.equal(leaderShown('bossPrep'), true);
|
||
// the left player of the pair: its own half, unmirrored
|
||
assert.deepEqual(m.handle('p_3', { t: 'g.watch', fieldId: 'n:p_0' }), { ok: true });
|
||
const left = h.lastTo('p_3', 'm.field');
|
||
assert.deepEqual([left.kind, left.side], ['boss', 'L']);
|
||
// its own pieces on the left half (since item 51 the partner p_1's board stands on the right half beside them)
|
||
for (const x of left.units.filter((x) => x.ownerId === 'p_0')) assert.ok(x.x <= 10, 'the left half');
|
||
assert.ok(left.units.some((x) => x.ownerId === 'p_1' && x.uid === onBoard.uid && x.x === want.col), 'p_1\'s piece on the right half');
|
||
m.dispose();
|
||
});
|
||
|
||
test('the eliminated viewer is pushed the boss-field scout at the boss round\'s start (item 56); a normal round\'s scout stays the normal board', () => {
|
||
const h = makeMatch({ mode: 'coop', difficulty: 'FUNNY', humans: 3, seed: 56, fake: true }).start();
|
||
const m = h.m;
|
||
m.bossPairs = [['p_0', 'p_1'], ['p_2']]; // keep this scout fixture's named left/right roles
|
||
h.toPrep(1);
|
||
h.ps('p_2').eliminate(1);
|
||
h.toPrep(2);
|
||
const normal = h.lastTo('p_2', 'm.field');
|
||
assert.deepEqual([normal.fieldId, normal.kind, normal.prep, normal.side], ['n:p_0', 'normal', true, undefined]);
|
||
assert.deepEqual(normal.rect, GEO.NORMAL_RECT);
|
||
h.drive(() => m.phase === PHASE.PREP && m.round === m.gd.bossRound, { ready: true });
|
||
const meta = h.lastTo('p_2', 'm.field');
|
||
assert.deepEqual([meta.fieldId, meta.kind, meta.prep, meta.side], ['n:p_0', 'boss', true, 'L']);
|
||
assert.ok(leaderStand(meta.nextEnemies, () => null, (x, y) => [[y, x]]), 'the leader at its spawn');
|
||
m.dispose();
|
||
});
|