feedback5: merge R19B — GitHub #236 (an eliminated player / spectator seat sees 观战中, not 资金已到账; idea from PR #237 by @2321Robin) and #173 (干员调配 says when its game data did not load; imports refused before anything is applied); golden unchanged

This commit is contained in:
sganggs committed 2026-10-07 11:27:12 +08:00
commit 15b529388f
9 files changed
+235 -8

No files matched your search

+2
View File
@@ -82,6 +82,7 @@
- 持续的范围特效(如炼金单元)叠在一起时不再越叠越亮(社区反馈)。
- 结算卡不再显示调和、协防干员、独行、绝技的层数:这几个盟约照常累计层数,官方也不显示(社区反馈)。
- 银灰的普通攻击使用自己的命中音效,不再借用三技能的挥砍和命中音效;雷蛇、初雪、圣约送葬人的普通攻击也不再用技能的命中音效(社区反馈)。
- 游戏数据没能下载时,「干员调配」不再显示空的干员列表,导入也不再提示「这份数据在当前版本没有可用的调配」:现在会直接写明游戏数据没有载入、缺的是哪个文件,并提示刷新页面、检查广告拦截插件和网络。这时三个标签页都不执行导入,已保存的干员持有和自选编队也不会再被清空(GitHub #173,感谢 @sarilendorf)。
### 联机与大厅
@@ -89,6 +90,7 @@
- 被淘汰后观战(以及观战席)会一直跟着上次查看的玩家,每回合开始不再跳回自己的空棋盘;没查看过时跟随第一位仍在场的玩家,人机也算(社区反馈;思路来自 PR #189,感谢 @2321Robin)。
- 观战领袖回合的休整期时,看到的是领袖战场和站在那里的领袖,不再是没有领袖的普通棋盘(社区反馈)。
- 最终攻势和隐秘核心的休整期,像作战时一样把同组两名玩家放在同一个战场上(队友的一半只能看),从回合开始到作战结束,左侧队伍栏用官方的绿框标出同组两人(社区反馈;参考了 PR #191,感谢 @2321Robin)。
- 被淘汰后(以及观战席)每回合开始的横幅改为显示「观战中」,不再显示「资金已到账」:回合资金只发给仍在场的玩家(GitHub #236;思路来自 PR #237,感谢 @2321Robin)。
### 人机
+46
View File
@@ -1856,3 +1856,49 @@ are.
intents too (+3 layers, ATK, block). Two NORMAL copies of one item merge (the official rule), so two 空弦 need a normal
and an 进阶 set; and the pair counts once toward the 3 members.
- Pinned: `test/content/feedback5-cb1-pins.test.js`.
#### 25.22.2 GitHub #236 「被淘汰后每回合开始横幅仍显示「资金已到账」(观战席同理)」 (@2321Robin, with PR #237): the round-start banner reads 观战中 to an eliminated player and a spectator seat — `ui/gameLogic/phases.js phaseBanner`, `screens/game.js`
- **Seen** (a real Match with fake battles: three humans and a spectator seat, p_0 out of LP at round 1's settlement):
at the round-2 start p_0 (funds 0, pending 0) and the spectator seat (no seat, no funds) got 「第 2 回合 · 资金已到账」,
the same as p_1 (funds 5). The sub had been fixed text since 0.1.0.
- **Official**: no rule for the banner's text. The money: round income goes only to the players still in.
`startRound` runs `ps.startRound` (income + pending coins) for `alivePlayers()` only, and `eliminate()` zeroes
`funds` / `pendingFunds` (server/match/match/phases.js, player/round.js). An eliminated player keeps watching
(research 09: KEEP_WATCH).
- **Now**: `phaseBanner(phase, pub, { alive, spectator })`. The match screen passes its `alive` and `spectator`.
`alive` is false for an eliminated player and for a spectator seat. A spectator seat has no row in m.public, so `pub`
alone cannot tell. For those viewers ROUND_START's sub is 「观战中」 (en "Spectating", the combat HUD's spectator
caption). A player still in keeps 「资金已到账」. The title and the other phases do not change. PR #237 was read for the
idea only: its fourth `alive` argument sits on top of the #189 / #191 stack and Match.js changes, so it was not merged.
- Golden: unchanged (client text only). Tests: `test/ui/gameLogic.test.js` +1 (a player still in, an eliminated
player, a spectator seat, the screen's call). It fails on the parent: 「资金已到账」 where 「观战中」 is expected.
#### 25.22.3 GitHub #173 「bug:网页版游戏文件未加载」 (@sarilendorf): a 干员调配 tab without its game data says so, and every import is refused — `screens/loadout.js missingGameData / importRefusal / DataMissing`
- **Seen**: on the reporter's own server (Node 24) the operators, bonds, maps and enemies did not load. Servers other
people host worked, and a loadout exported there failed to import locally. The screenshots, as described in the
thread: 干员调配 empty, the import toast 「导入失败:这份数据在当前版本没有可用的调配,未做任何改动」, the `npm start` log
with every check passed, an ad blocker showing one block. Reproduced with every /data/*.json request answering 404. All five files of the screen end
`missing`, the screen counts them as loaded, the roster is empty (「没有符合条件的干员」) and the loadout import gives the
reporter's toast. Two imports did worse: 干员持有 said 「已导入:全部持有」 and stored [] over the saved list, and
自选编队 said 「已导入:全部名额为空」 and stored {} over the saved picks. With the files served, the same three imports
work. 0.1.3 and 0.1.4 behave the same.
- **Cause**: `useData` (public/js/data.js:489–501) counts a file that never arrived (status `missing`) as settled, so
the UI can carry on without optional files. In screens/loadout.js `ready` (:399) was then true. The tab drew its empty
roster, and the import guard `if (!ready)` (:479) let the import run. It exists to stop an import before chess.json
loads, because such an import would sanitise every entry away. The loadout import kept the loadout but blamed the data
(:501). `applyOwnershipImport` / `applyDiyImport` (ui/loadoutSync.js:93, :113) stored the empty result. The server sync
was already safe: the loadout sync sends nothing without chess.json, and the other two send the stored lists.
- **Official**: no rule involved; the page reads its game data from the server it was opened from (/data/*.json).
- **Now**: `missingGameData(tab)` names the files a tab cannot work without: chess.json, plus backups.json for 自选编队.
When one is `missing`, the tab shows `DataMissing` in place of the roster: 「游戏数据没有载入」, the file paths (e.g.
/data/chess.json), that the saved settings are unaffected, and what to try (reload; then the ad blocker and the
network). `importRefusal(kind, ready)` is the one guard of every import. It refuses while the files load (as before) and
when the data is missing (「导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络」),
before anything is parsed or applied. Three new UI strings, with English.
- [ASSUMED]: the cause on the reporter's machine is still unknown: the ad blocker, a proxy or security software, or a
different address. The new message and the console line `[data] /data/<file> unavailable (<reason>)` are what to ask for.
- Golden: unchanged (client only). Tests: `test/ui/feedback5-loadout-data-missing.test.js` (4, all fail on the parent:
the tab's files, the refusals, the screen's guard order, the message in Chinese and English); loadout, loadout-stats,
module-trait, moduleIcons, diy-ui and standin-ui tests pass unchanged.
+4
View File
@@ -73,6 +73,10 @@
.lo-note.is-locked .icon { color: var(--amber); }
.lo-loading { flex: 1; display: flex; align-items: center; justify-content: center; gap: .14rem; color: var(--text-lo); font-size: max(.18rem, 12px); }
/* the tab's game data did not load (GitHub #173): said in place of the roster */
.lo-missing { flex-direction: column; gap: .1rem; padding: .3rem; text-align: center; color: var(--amber); }
.lo-missing .icon { width: max(.32rem, 22px); height: max(.32rem, 22px); }
.lo-missing p { margin: 0; max-width: 9rem; color: var(--text-lo); font-size: max(.16rem, 11px); line-height: 1.6; text-wrap: balance; }
/* ---- body layout -------------------------------------------------------------------------------------------------- */
.lo-body {
+3
View File
@@ -295,6 +295,9 @@
"读取文件失败": "Failed to read the file",
"干员数据仍在载入,请稍候再导入": "Operator data is still loading, please wait before importing",
"导入失败:这份数据在当前版本没有可用的调配,未做任何改动": "Import failed: this data has no loadout usable in the current version. Nothing was changed",
"导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络": "Import failed: the game data did not load. Nothing was changed. Reload the page; if that does not help, check your ad blocker and your network",
"游戏数据没有载入": "The game data did not load",
"{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。": "{files} could not be downloaded, so this page cannot be shown. Your saved settings are unaffected. Reload the page; if that does not help, check your ad blocker and your network.",
"确认本局信息阶段结束前可调整本局配置": "You can adjust this match's loadout until the match briefing ends",
"开始模拟前可调整干员携带的技能与模组,干员等级不可调整": "Before the simulation starts you can change the Skills and Modules your Operators carry; Operator levels cannot be changed",
"返回 (Esc)": "Back (Esc)",
+1 -1
View File
@@ -576,7 +576,7 @@ function MatchScreen() {
const prev = prevPhase.current;
prevPhase.current = phase;
if (prev === phase) return;
const b = phaseBanner(phase, pub);
const b = phaseBanner(phase, pub, { alive, spectator });
if (b) setBanner({ ...b, key: phaseKey });
if (phase === PHASE.ROUND_START) audio.sfx('roundStart');
else if (phase === PHASE.PREP) audio.sfx('rest', { volume: 0.7 });
+45 -4
View File
@@ -24,7 +24,7 @@ import { Img, RichText, UnitThumb } from '../ui/gameComponents.js';
import { chessAvatarUrl, chessPortraitUrl, subProfIconUrl, bondIconUrl, moduleTypeIconUrl } from '../ui/assetUrls.js';
import { chessStatsBlock, traitText, chessTalents } from '../ui/detailPanel.js';
import { chessLoadout } from '../ui/gameLogic.js';
import { data, useData, localAsset } from '../data.js';
import { data, useData, localAsset, DATA_FILES } from '../data.js';
import { useStore } from '../store.js';
import { PHASE } from '../../../shared/constants.js';
import {
@@ -394,6 +394,43 @@ const OWN_SYNC_TEXT = { ...SYNC_TEXT, locked: [N_('下一局生效'), 'is-warn']
/** The 自选编队 tab's status line (msgids, translated where shown): out of match, like 干员持有. */
const DIY_SYNC_TEXT = { ...OWN_SYNC_TEXT };
/**
* The game-data files a tab cannot work without — its roster and its import's sanitiser: chess.json, plus backups.json
* for 自选编队 (its slots and the picks' forms) — that did not load. useData counts a file that never arrived (a 404, a
* blocked request, bad JSON: status 'missing') as settled like a loaded one, so the tab drew an empty roster and an
* import sanitised every entry away (GitHub #173). Empty while the files load and once they have.
* @param {string} tab 'loadout' | 'ownership' | 'diy'
* @param {(name: string) => string} [status] the data store's status (data.status)
* @returns {string[]} data file names
*/
export function missingGameData(tab, status = data.status) {
return (tab === 'diy' ? ['chess', 'backups'] : ['chess']).filter((name) => status(name) === 'missing');
}
/**
* Why an import of `kind` cannot run now, as its toast — the files still loading, or the game data missing — or null.
* Either way nothing is applied: sanitised against absent data, the 干员持有 / 自选编队 imports stored an empty list over
* the saved one (GitHub #173).
* @param {string} kind the dialog's tab @param {boolean} ready the screen's files are settled
* @param {(name: string) => string} [status]
* @returns {{ text: string, tone: 'warn'|'error' } | null}
*/
export function importRefusal(kind, ready, status = data.status) {
if (!ready) return { text: t('干员数据仍在载入,请稍候再导入'), tone: 'warn' };
if (missingGameData(kind, status).length) return { text: t('导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络'), tone: 'error' };
return null;
}
/** In place of a tab whose game data did not load (missingGameData): which files, and what to try (GitHub #173). */
export function DataMissing({ files }) {
const urls = files.map((name) => `/data/${DATA_FILES[name] || `${name}.json`}`);
return html`<div class="lo-loading lo-missing" role="alert" data-testid="loadout-data-missing">
<${Icon} name="warn" />
<b>${t('游戏数据没有载入')}</b>
<p>${t('{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。', { files: urls })}</p>
</div>`;
}
/** The overlay screen. */
function LoadoutScreen({ st }) {
const ready = useData('chess', 'bonds', 'assets', 'local', 'backups');
@@ -422,6 +459,7 @@ function LoadoutScreen({ st }) {
const [io, setIo] = useState(null); // 导出 / 导入 dialog: { mode, text } | null
const tab = st.tab === 'ownership' || st.tab === 'diy' ? st.tab : 'loadout';
const lost = ready ? missingGameData(tab) : [];
const ownRoster = useOwnershipRoster(ready);
const nNotOwned = notOwnedCount(st.notOwned, ownRoster);
// 自选编队 (0.2.0 DIY): the stored picks, those the server would keep (the kit list of the last welcome)
@@ -475,8 +513,10 @@ function LoadoutScreen({ st }) {
try { setIo({ ...io, mode: 'import', text: await readFileText(f) }); } catch { toast(t('读取文件失败'), 'error'); }
};
const ioApply = () => {
// an import before chess.json is loaded would sanitise every entry away — refuse instead of wiping the loadout
if (!ready) { toast(t('干员数据仍在载入,请稍候再导入'), 'warn'); return; }
// an import before chess.json is loaded, or when it never arrived, would sanitise every entry away — refuse instead
// of wiping the loadout
const refused = importRefusal(io.kind, ready);
if (refused) { toast(refused.text, refused.tone); return; }
if (ioDiy) {
const r = parseDiyImport(ioText);
if (!r.ok) { toast(t('导入失败:{error}', { error: t(r.error, r.params) }), 'error'); return; }
@@ -588,7 +628,8 @@ function LoadoutScreen({ st }) {
: tab === 'ownership'
? html`<p class=${cx('lo-note', ownLocked && 'is-locked')}><${Icon} name="info" />${ownText}</p>`
: html`<p class=${cx('lo-note', locked && 'is-locked')}><${Icon} name="info" />${locked ? t('本局的调配已锁定(确认本局信息后无法修改),修改将在下一局生效') : fromText}</p>`}
${!ready ? html`<div class="lo-loading"><${Spinner} size="sm" />${t('正在载入干员数据(打开页面后仅载入一次)…')}</div>` : tab === 'diy'
${!ready ? html`<div class="lo-loading"><${Spinner} size="sm" />${t('正在载入干员数据(打开页面后仅载入一次)…')}</div>`
: lost.length ? html`<${DataMissing} files=${lost} />` : tab === 'diy'
? html`<${DiyPanel} m=${m} picks=${st.diy || {}} legal=${diyLegal} kitted=${st.diyKitted} onSet=${setDiySlot} />`
: tab === 'ownership'
? html`<${OwnershipPanel} m=${m} roster=${ownRoster} notOwned=${st.notOwned} onToggle=${toggleOwned} />`
+12 -3
View File
@@ -40,12 +40,21 @@ export const isCombatPhase = (phase) => COMBAT_PHASES.has(phase);
export const showDeadPill = (alive, phase) => !alive && !COMBAT_PHASES.has(phase) && phase !== PHASE.SETTLE;
export const isBossPhase = (phase) => phase === PHASE.FINAL_ASSAULT || phase === PHASE.HIDDEN_CORE;
/** Banner shown when a phase starts: { title, sub?, tone } or null. */
export function phaseBanner(phase, pub) {
/**
* Banner shown when a phase starts: { title, sub?, tone } or null.
* @param {string|null|undefined} phase
* @param {any} pub m.public
* @param {{ alive?: boolean, spectator?: boolean }} [viewer] the local viewer (screens/game.js): `alive` false for an
* eliminated player, `spectator` for a spectator seat — it has no row in m.public, so `pub` cannot tell
*/
export function phaseBanner(phase, pub, { alive = true, spectator = false } = {}) {
const r = int(pub?.round, 0);
switch (phase) {
case PHASE.BATTLE_CHECK: return { title: t('协议启动'), micro: 'PROTOCOL START', tone: 'mint', sub: t('模拟即将开始'), duration: 2600 };
case PHASE.ROUND_START: return { title: t('第 {r} 回合', { r }), micro: `ROUND ${String(r).padStart(2, '0')}`, tone: 'mint', sub: t('资金已到账') };
// round income goes to the seats still in only (server/match/match/phases.js startRound; eliminate() zeroes funds
// and pendingFunds): an eliminated player or a spectator seat reads 观战中, never 资金已到账 (GitHub #236, PR #237)
case PHASE.ROUND_START: return { title: t('第 {r} 回合', { r }), micro: `ROUND ${String(r).padStart(2, '0')}`, tone: 'mint',
sub: spectator || alive === false ? t('观战中') : t('资金已到账') };
case PHASE.SP_DRAFT: return { title: t('机变阶段'), micro: 'CONTINGENCY', tone: 'gold', sub: t('依次选择机变') };
case PHASE.PREP: return { title: t('休整期'), micro: `ROUND ${String(r).padStart(2, '0')} // REST`, tone: 'mint', sub: t('部署干员,准备迎敌') };
case PHASE.COMBAT: return { title: t('作战开始'), micro: 'COMBAT', tone: 'orange', sub: t('各自行动阶段') };
@@ -0,0 +1,99 @@
// GitHub #173 「网页版游戏文件未加载」: the page runs but its game data never arrives (every /data/*.json request fails —
// a blocked request, a proxy, a server without its data folder). useData counts a file that failed ('missing') as
// settled like a loaded one, so 干员调配 drew an empty roster (「没有符合条件的干员」) and the 导入 of a loadout said the data
// had no loadout usable in this version; the 干员持有 / 自选编队 imports, sanitised against the absent data, stored an
// empty list over the saved one. Now the tab says that the game data did not load (which files, what to try) and every
// import is refused with the same words, before anything is parsed or applied — the saved settings stay as they are.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
// the reporter's case: every data request of the page fails (the screen reads the browser data store singleton)
globalThis.fetch = async () => ({ ok: false, status: 404, json: async () => ({}) });
const warn = console.warn;
console.warn = (...a) => { if (!String(a[0]).startsWith('[data]')) warn(...a); };
const { data } = await import('../../public/js/data.js');
const { missingGameData, importRefusal, DataMissing } = await import('../../public/js/screens/loadout.js');
const { setLang, setMessages } = await import('../../shared/i18n.js');
const TABS = ['loadout', 'ownership', 'diy'];
const statusOf = (map, dflt = 'ready') => (name) => map[name] ?? dflt;
const NOT_LOADED = '导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络';
const LOADING = '干员数据仍在载入,请稍候再导入';
function* walk(v) {
if (Array.isArray(v)) { for (const x of v) yield* walk(x); return; }
if (!v || typeof v !== 'object') return;
yield v;
yield* walk(v.props?.children);
}
const textOf = (v) => [...walk(v)].flatMap((n) => (Array.isArray(n.props?.children) ? n.props.children : [n.props?.children]))
.filter((x) => typeof x === 'string').map((s) => s.trim()).filter(Boolean).join(' | ');
test('a data file that never arrived is settled like a loaded one: the tab names the files it cannot work without', async () => {
await data.loadAll('chess', 'bonds', 'assets', 'local', 'backups'); // the screen's useData(...) files
for (const name of ['chess', 'bonds', 'assets', 'local', 'backups']) assert.equal(data.status(name), 'missing', name);
// (default: the browser store) chess.json for every tab, backups.json too for 自选编队 (its slots and picks' forms)
assert.deepEqual(missingGameData('loadout'), ['chess']);
assert.deepEqual(missingGameData('ownership'), ['chess']);
assert.deepEqual(missingGameData('diy'), ['chess', 'backups']);
for (const tab of TABS) {
assert.deepEqual(missingGameData(tab, statusOf({})), [], `${tab}: loaded`);
assert.deepEqual(missingGameData(tab, statusOf({}, 'loading')), [], `${tab}: still loading (the spinner, not this message)`);
}
// only the art / bond files missing: every tab still works
for (const tab of TABS) assert.deepEqual(missingGameData(tab, statusOf({ bonds: 'missing', assets: 'missing', local: 'missing' })), []);
const noBackups = statusOf({ backups: 'missing' });
assert.deepEqual(TABS.map((tab) => missingGameData(tab, noBackups)), [[], [], ['backups']]);
});
test('every import is refused while the files load or when the game data did not load — no other reason', () => {
const missing = statusOf({ chess: 'missing', backups: 'missing' });
for (const kind of TABS) {
assert.deepEqual(importRefusal(kind, false, statusOf({})), { text: LOADING, tone: 'warn' }, `${kind}: loading`);
assert.deepEqual(importRefusal(kind, true, missing), { text: NOT_LOADED, tone: 'error' }, `${kind}: not loaded`);
assert.deepEqual(importRefusal(kind, true), { text: NOT_LOADED, tone: 'error' }, `${kind}: the browser store`);
assert.equal(importRefusal(kind, true, statusOf({})), null, `${kind}: loaded`);
}
const noBackups = statusOf({ backups: 'missing' });
assert.equal(importRefusal('loadout', true, noBackups), null);
assert.equal(importRefusal('ownership', true, noBackups), null);
assert.deepEqual(importRefusal('diy', true, noBackups), { text: NOT_LOADED, tone: 'error' });
});
test('the screen: the import asks importRefusal before parsing or applying anything; a tab without its data shows DataMissing', () => {
const src = readFileSync(path.join(ROOT, 'public/js/screens/loadout.js'), 'utf8');
const at = src.indexOf('const ioApply = () => {');
assert.ok(at > 0, 'ioApply');
const body = src.slice(at, src.indexOf('\n };\n', at));
const guard = body.indexOf('const refused = importRefusal(io.kind, ready);');
assert.ok(guard > 0, 'the guard');
assert.match(body.slice(guard), /^const refused = importRefusal\(io\.kind, ready\);\s*if \(refused\) \{ toast\(refused\.text, refused\.tone\); return; \}/);
for (const call of ['parseDiyImport(', 'applyDiyImport(', 'parseOwnershipImport(', 'applyOwnershipImport(', 'parseImport(', 'applyLoadoutEntries(']) {
assert.ok(body.indexOf(call) > guard, `${call} only after the guard`);
}
assert.ok(!/if \(!ready\)/.test(body), 'one guard for both cases');
assert.match(src, /const lost = ready \? missingGameData\(tab\) : \[\];/);
assert.match(src, /: lost\.length \? html`<\$\{DataMissing\} files=\$\{lost\} \/>` : tab === 'diy'/);
});
test('the message names the files and what to try, in Chinese and in English', () => {
const one = DataMissing({ files: ['chess'] });
assert.equal(one.props.role, 'alert');
assert.equal(textOf(one), '游戏数据没有载入 | /data/chess.json 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。');
assert.match(textOf(DataMissing({ files: ['chess', 'backups'] })), /\/data\/chess\.json、\/data\/backups\.json 没有下载成功/);
const en = JSON.parse(readFileSync(path.join(ROOT, 'public/i18n/en.json'), 'utf8'));
setMessages('en', en);
setLang('en');
try {
assert.equal(textOf(DataMissing({ files: ['chess', 'backups'] })), 'The game data did not load | /data/chess.json, /data/backups.json could not be downloaded, '
+ 'so this page cannot be shown. Your saved settings are unaffected. Reload the page; if that does not help, check your ad blocker and your network.');
assert.equal(importRefusal('loadout', true).text, 'Import failed: the game data did not load. Nothing was changed. Reload the page; if that does not help, check your ad blocker and your network');
} finally {
setLang('zh');
}
});
+23
View File
@@ -72,6 +72,29 @@ describe('phases', () => {
assert.equal(phaseBanner(PHASE.SETTLE, {}), null);
assert.equal(prepCapsuleLabel(PHASE.PREP), '休息一下');
});
test('the round-start banner says 资金已到账 to a player still in only — 观战中 to an eliminated player and a spectator seat (GitHub #236)', () => {
// round 2 after p_0 ran out of LP: p_1 is still in; the spectator seat s_spec has no row in m.public
const pub = { round: 2, phase: PHASE.ROUND_START, players: [
{ playerId: 'p_0', seat: 0, alive: false }, { playerId: 'p_1', seat: 1, alive: true },
] };
const round = (viewer) => phaseBanner(PHASE.ROUND_START, pub, viewer);
// a player still in (the server paid the round's income)
assert.equal(round({ alive: true, spectator: false }).sub, '资金已到账');
assert.equal(round().sub, '资金已到账', 'no viewer given: the copy of a player still in');
// an eliminated player (eliminate() zeroed funds and pendingFunds; startRound pays the seats still in only)
assert.equal(round({ alive: false, spectator: false }).sub, '观战中');
// a spectator seat (no seat, no funds), also when only `spectator` is given
assert.equal(round({ alive: false, spectator: true }).sub, '观战中');
assert.equal(round({ spectator: true }).sub, '观战中');
// the title and the other phases' copy are the same for every viewer
for (const v of [{ alive: true }, { alive: false }, { alive: false, spectator: true }]) {
assert.equal(round(v).title, '第 2 回合');
assert.equal(phaseBanner(PHASE.PREP, pub, v).sub, phaseBanner(PHASE.PREP, pub).sub);
}
// the match screen hands the banner its viewer (screens/game.js `alive` folds a spectator seat in)
const src = readFileSync(path.join(ROOT, 'public/js/screens/game.js'), 'utf8');
assert.match(src, /phaseBanner\(phase, pub, \{ alive, spectator \}\)/);
});
});
describe('countdown', () => {