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:
commit
15b529388f
9 files changed
+235
-8
No files matched your search
@@ -82,6 +82,7 @@
|
||||
- 持续的范围特效(如炼金单元)叠在一起时不再越叠越亮(社区反馈)。
|
||||
- 结算卡不再显示调和、协防干员、独行、绝技的层数:这几个盟约照常累计层数,官方也不显示(社区反馈)。
|
||||
- 银灰的普通攻击使用自己的命中音效,不再借用三技能的挥砍和命中音效;雷蛇、初雪、圣约送葬人的普通攻击也不再用技能的命中音效(社区反馈)。
|
||||
- 游戏数据没能下载时,「干员调配」不再显示空的干员列表,导入也不再提示「这份数据在当前版本没有可用的调配」:现在会直接写明游戏数据没有载入、缺的是哪个文件,并提示刷新页面、检查广告拦截插件和网络。这时三个标签页都不执行导入,已保存的干员持有和自选编队也不会再被清空(GitHub #173,感谢 @sarilendorf)。
|
||||
|
||||
### 联机与大厅
|
||||
|
||||
@@ -89,6 +90,7 @@
|
||||
- 被淘汰后观战(以及观战席)会一直跟着上次查看的玩家,每回合开始不再跳回自己的空棋盘;没查看过时跟随第一位仍在场的玩家,人机也算(社区反馈;思路来自 PR #189,感谢 @2321Robin)。
|
||||
- 观战领袖回合的休整期时,看到的是领袖战场和站在那里的领袖,不再是没有领袖的普通棋盘(社区反馈)。
|
||||
- 最终攻势和隐秘核心的休整期,像作战时一样把同组两名玩家放在同一个战场上(队友的一半只能看),从回合开始到作战结束,左侧队伍栏用官方的绿框标出同组两人(社区反馈;参考了 PR #191,感谢 @2321Robin)。
|
||||
- 被淘汰后(以及观战席)每回合开始的横幅改为显示「观战中」,不再显示「资金已到账」:回合资金只发给仍在场的玩家(GitHub #236;思路来自 PR #237,感谢 @2321Robin)。
|
||||
|
||||
### 人机
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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} />`
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in new issue
Block a user