Files
ark-stronghold-protocol/test/match/observe.test.js
T
xcdoge b4bc89ad4e feedback7 UIBACK: the top-bar capsule counts like the official — resolved = killed + leaked among the enemies the round scheduled (opening 0/3, a leak 1/3, a kill 2/3, the splitter 3/3); a split child, a summon and a boss summon are in neither number but still cost LP and break 完美作战 (PR #308 by @xcdoge, the capsule half of PR #157 — authored by them: the sim / protocol / HUD hunks applied as reviewed): inTotal / killedInTotal / leakedInTotal / resolved, b.snap / BattleResult / b.progress (optional field) / m.public progress carry resolved, killed may exceed total (validation bounds it by the content-spawn allowance); dropped the PR's stray design-doc line — it had replaced the S→C b.end bullet of docs/design/network.md, which is back; the 联防 live-counter change mark follows the enemy list now that a spawn no longer moves total (uniteLeftMark + test); golden: only total / players.*.total / snaps move, outcomes bit-identical — roster 23 (roster-002 003 007 008 009 010 011 012 013 015 017 018 020 023 024 025 026 027 037 045 047 048 049), bonds 2 (bond-maniShip-low, bond-soloShip-high), fields 11 (boss-boss_1 3 5 pair+solo, boss-boss_6-solo, hidden-boss_8 10 pair+solo), diy 11 (diy-036 040 056 062 068 073 083 091 095 114 115), matches 12 (solo-FUNNY-1 NORMAL-1 HARD-1 ABYSS-1 ABYSS-2, coop2-NORMAL-3 coop2-HARD-4 coop3-ABYSS-5 coop4-ABYSS-7 coop2-NORMAL-8-serverrun solo-HARD-9-boosted coop2-NORMAL-70-diy: the total column of 36 round rows), standins unchanged
2026-10-08 19:30:36 +08:00

85 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Observing rules of the client (public/js/battle/observe.js, research 09 §3.1 / §6.3) — the mirror of the server's
// Match._watchClient: prep boards, no observing while the own normal battle runs, a teammate's field once it is over,
// the other pair's boss field hidden, eliminated players free; the 联防 / 最终攻势 camera halves and their captions;
// a reloaded screen that watched a teammate's battle takes the watch back (resumedWatch).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { observeTarget, teammateProgress, cameraLayers, layerCamera, isClientCombat, resumedWatch } from '../../public/js/battle/observe.js';
const P = (id, seat, extra = {}) => ({ playerId: id, seat, name: id.toUpperCase(), alive: true, ...extra });
const pubOf = (phase, fields, players) => ({ phase, combatMode: 'client', fields, players });
test('observeTarget: prep, own battle running / over, boss pairs, eliminated players', () => {
const players = [P('a', 0), P('b', 1), P('c', 2, { alive: false })];
assert.equal(isClientCombat(pubOf('PREP', [], players)), true);
assert.deepEqual(observeTarget(players[1], pubOf('PREP', [], players), 'a'), { fieldId: 'n:b' });
assert.match(observeTarget(players[2], pubOf('PREP', [], players), 'a').reason, /淘汰/);
const combat = (liveA) => pubOf('COMBAT', [
{ fieldId: 'n:a', kind: 'normal', players: ['a'], live: liveA },
{ fieldId: 'n:b', kind: 'normal', players: ['b'], live: true, progress: { killed: 3, total: 8, done: false } },
], players);
assert.match(observeTarget(players[1], combat(true), 'a').reason, /作战中无法查看/);
assert.deepEqual(observeTarget(players[1], combat(true), 'a', { ownDone: true }), { fieldId: 'n:b' }, 'the local battle already ended');
assert.deepEqual(observeTarget(players[1], combat(false), 'a'), { fieldId: 'n:b' });
assert.deepEqual(observeTarget(players[0], combat(false), 'a', { observing: true }), { back: true });
// the teammate's capsule reads `resolved` (the field's own enemies knocked out or leaked); a report without one falls
// back to `killed` (public/js/battle/observe.js teammateProgress)
assert.deepEqual(teammateProgress(combat(false), 'a'), [{ playerId: 'b', name: 'B', isBot: false, killed: 3, resolved: 3, total: 8, done: false }]);
const reported = pubOf('COMBAT', [{ fieldId: 'n:b', kind: 'normal', players: ['b'], live: true, progress: { killed: 5, resolved: 3, total: 8, done: false } }], players);
assert.deepEqual(teammateProgress(reported, 'a'), [{ playerId: 'b', name: 'B', isBot: false, killed: 5, resolved: 3, total: 8, done: false }],
'a reported `resolved` wins over `killed` (5 counted knock-outs — splits included —, 3 of the round\'s own enemies resolved)');
const fa = pubOf('FINAL_ASSAULT', [
{ fieldId: 'b1', kind: 'boss', players: ['a', 'b'], live: true },
{ fieldId: 'b2', kind: 'boss', players: ['d'], live: true },
], [...players, P('d', 3)]);
assert.match(observeTarget(P('d', 3), fa, 'a').reason, /另一组/);
assert.match(observeTarget(players[1], fa, 'a').reason, /同一战场/);
const dead = pubOf('FINAL_ASSAULT', fa.fields, [{ ...players[0], alive: false }, players[1], P('d', 3)]);
assert.deepEqual(observeTarget(P('d', 3), dead, 'a'), { fieldId: 'b2' }, 'eliminated: anything');
});
test('cameraLayers: ‹ LEFT / 全景 / RIGHT › with "你自己" / name / "无人在家"; none for normal or single boss fields', () => {
const pub = { players: [P('a', 0), P('b', 1)] };
const unite = { fieldId: 'u', kind: 'unite', rect: { r0: 9, r1: 12, c0: 0, c1: 20 }, players: ['a', 'b'], sides: { a: 'R', b: 'L' } };
assert.deepEqual(cameraLayers(unite, pub, 'a').map((l) => [l.key, l.label]), [['L', 'B'], ['ALL', '全景'], ['R', '你自己']]);
const single = { ...unite, players: ['b'], sides: { b: 'L' } };
assert.deepEqual(cameraLayers(single, pub, 'a').map((l) => l.label), ['B', '全景', '无人在家']);
assert.deepEqual(cameraLayers({ kind: 'normal' }, pub, 'a'), []);
assert.deepEqual(cameraLayers({ kind: 'boss', players: ['a'], sides: { a: 'L' } }, pub, 'a'), [], 'a solo boss field has no halves');
assert.deepEqual(layerCamera(unite, 'R', 'R'), { rect: unite.rect, side: 'R', half: true });
assert.deepEqual(layerCamera(unite, 'ALL', 'R'), { rect: unite.rect, side: 'R' });
});
test('resumedWatch: a reload while watching a teammate\'s battle adopts the resent field once per battle (review, DESIGN §20.15)', () => {
const players = [P('a', 0), P('b', 1), P('c', 2)];
const pub = pubOf('COMBAT', [
{ fieldId: 'n:a', kind: 'normal', players: ['a'], live: false },
{ fieldId: 'n:b', kind: 'normal', players: ['b'], live: true },
], players);
const watched = { battleId: 'r3-n:b', fieldId: 'n:b', kind: 'normal', watch: true, done: false };
const own = { battleId: 'r3-n:a', fieldId: 'n:a', kind: 'normal', watch: false, done: true };
const o = { pub, myId: 'a', alive: true, watching: null, seen: null };
// the reload: the server resends b's battle (b.start watch: true) to a screen at home
assert.deepEqual(resumedWatch(watched, o), { seen: 'r3-n:b', fieldId: 'n:b' });
// once per battle: after 返回战场 (watching null again) the same battle, still shown until the own b.start, stays put
assert.deepEqual(resumedWatch(watched, { ...o, seen: 'r3-n:b' }), { seen: 'r3-n:b', fieldId: null });
// first seen while watching (a 前往查看 under way): marked, never adopted later
assert.deepEqual(resumedWatch(watched, { ...o, watching: 'n:b' }), { seen: 'r3-n:b', fieldId: null });
// loading (no `watch` in that state yet): undecided while watching nothing, marked when already watching
const loading = { loading: true, battleId: 'r3-n:b', fieldId: 'n:b', kind: 'normal' };
assert.deepEqual(resumedWatch(loading, o), { seen: null, fieldId: null });
assert.deepEqual(resumedWatch(loading, { ...o, watching: 'n:b' }), { seen: 'r3-n:b', fieldId: null });
// the own battle, no battle, no m.public yet (undecided)
assert.deepEqual(resumedWatch(own, o), { seen: 'r3-n:a', fieldId: null });
assert.deepEqual(resumedWatch({ ...watched, fieldId: 'n:a' }, o), { seen: 'r3-n:b', fieldId: null }, 'a field listing the viewer');
assert.deepEqual(resumedWatch(null, o), { seen: null, fieldId: null });
assert.deepEqual(resumedWatch(watched, { ...o, pub: null }), { seen: null, fieldId: null });
// other rules keep theirs: an eliminated player's auto-observe, 联防 / boss fields, other phases, server-run combat
assert.deepEqual(resumedWatch(watched, { ...o, alive: false }), { seen: 'r3-n:b', fieldId: null });
const unite = pubOf('UNITE', [{ fieldId: 'u', kind: 'unite', players: ['b', 'c'], live: true }], players);
assert.deepEqual(resumedWatch({ battleId: 'r3-u', fieldId: 'u', kind: 'unite', watch: true }, { ...o, pub: unite }), { seen: 'r3-u', fieldId: null }, 'a 联防 leaker');
assert.deepEqual(resumedWatch(watched, { ...o, pub: { ...pub, phase: 'UNITE' } }), { seen: 'r3-n:b', fieldId: null });
assert.deepEqual(resumedWatch(watched, { ...o, pub: { ...pub, combatMode: 'server' } }), { seen: 'r3-n:b', fieldId: null });
assert.deepEqual(resumedWatch({ ...watched, fieldId: 'n:x' }, o), { seen: 'r3-n:b', fieldId: null }, 'a field not listed');
});