feedback5 WI: UI strings through t() with an English switch on the title screen and in 设置; pilot screens converted
public/i18n/en.json: Chinese msgid -> English, seeded from PR #70 (@YuriRestia, credited) for every Chinese literal of the current code (≈830 entries; 966 of the 1 036 literals left for phase 2 already translated), completed by hand in the official EN terms. public/js/ui/lang.js: boot language (?lang=en|zh, then localStorage sp.pref.lang, else Chinese; the browser language is not consulted), switchLang, useLang, LangToggle; main.js renders after initLang and re-renders in place on a switch, and translates server texts (m.toast / m.ticker frames, room.closed reasons, error codes via ui/toasts.js describeError). Converted: title screen (English title, wordmark hidden), lobby, room, settings (语言 row, On/Off, quality, hotkeys), the shell (crash screen, toasts) and DifficultyTag. tools/i18n.mjs: extract (msgids + untranslated literals per file), check (msgids missing from en.json), codemod (html`` text / attributes / strings / template literals -> t(), nested strings, N_ for module level, i18n-ignore lines), seed (fill en.json from a msgid map).
This commit is contained in:
1 parent
e596478bef
commit
a12a539e4d
13 files changed
+2102
-171
No files matched your search
+69
-61
@@ -8,6 +8,8 @@
|
||||
// screen is complete before data is generated. Rounds: solo 标准 = 9, everything else 14 (+R15
|
||||
// hidden core on 险境+), per research 00-INDEX §2. Battlefield pool (`modes[].stages`): 标准 always
|
||||
// plays 战场#01, 险境 draws one of 8, 绝境 / 终极 one of 7 (m01 excluded).
|
||||
// Texts go through t() (docs/I18N.md); the module-level tables hold msgids (N_) translated where they are shown, the
|
||||
// config.json mode texts come localized from data.js.
|
||||
|
||||
import { useEffect, useRef, useState } from '../../vendor/hooks.module.js';
|
||||
import { DIFFICULTIES, DIFFICULTY_NAMES, DIFFICULTY_COLORS, ROOM_CODE_LEN, MAX_SEATS, MAX_SPECTATORS, modeIdFor, ERR } from '../../../shared/constants.js';
|
||||
@@ -18,20 +20,21 @@ import { LoadoutButton } from './loadout.js';
|
||||
import { net, identity } from '../net.js';
|
||||
import { store, useStore, shallowEqual, loadPref, savePref } from '../store.js';
|
||||
import { getConfig, getMode, getStage, useData } from '../data.js';
|
||||
import { t, tc, N_ } from '../../../shared/i18n.js';
|
||||
|
||||
/** Official mode texts (activity_table act2autochess.modeDataDict), fallback when config.json is absent. */
|
||||
export const MODE_TEXT = {
|
||||
single: {
|
||||
FUNNY: { code: 'AC-1', desc: '时长较短的模拟训练', effects: ['可以快速完成作战', '常规奖励'] },
|
||||
NORMAL: { code: 'AC-2', desc: '敌方攻击强度较高的模拟训练', effects: ['可使用盟约数增加', '大幅增加奖励'] },
|
||||
HARD: { code: 'AC-3', desc: '敌方攻击强度极高的模拟训练', effects: ['作战环境困难', '出现更加危险的敌人'] },
|
||||
ABYSS: { code: 'AC-4', desc: '敌方攻击强度到达极限的模拟训练', effects: ['作战环境无比困难', '出现极度危险的敌人'] },
|
||||
FUNNY: { code: 'AC-1', desc: N_('时长较短的模拟训练'), effects: [N_('可以快速完成作战'), N_('常规奖励')] },
|
||||
NORMAL: { code: 'AC-2', desc: N_('敌方攻击强度较高的模拟训练'), effects: [N_('可使用盟约数增加'), N_('大幅增加奖励')] },
|
||||
HARD: { code: 'AC-3', desc: N_('敌方攻击强度极高的模拟训练'), effects: [N_('作战环境困难'), N_('出现更加危险的敌人')] },
|
||||
ABYSS: { code: 'AC-4', desc: N_('敌方攻击强度到达极限的模拟训练'), effects: [N_('作战环境无比困难'), N_('出现极度危险的敌人')] },
|
||||
},
|
||||
multi: {
|
||||
FUNNY: { code: 'AC-1', desc: '敌方攻击强度较低的模拟训练', effects: ['作战环境较为温和', '常规奖励'] },
|
||||
NORMAL: { code: 'AC-2', desc: '敌方攻击强度较高的模拟训练', effects: ['可使用盟约数增加', '大幅增加奖励'] },
|
||||
HARD: { code: 'AC-3', desc: '敌方攻击强度极高的模拟训练', effects: ['作战环境困难', '出现更加危险的敌人'] },
|
||||
ABYSS: { code: 'AC-4', desc: '敌方攻击强度到达极限的模拟训练', effects: ['作战环境无比困难', '出现极度危险的敌人'] },
|
||||
FUNNY: { code: 'AC-1', desc: N_('敌方攻击强度较低的模拟训练'), effects: [N_('作战环境较为温和'), N_('常规奖励')] },
|
||||
NORMAL: { code: 'AC-2', desc: N_('敌方攻击强度较高的模拟训练'), effects: [N_('可使用盟约数增加'), N_('大幅增加奖励')] },
|
||||
HARD: { code: 'AC-3', desc: N_('敌方攻击强度极高的模拟训练'), effects: [N_('作战环境困难'), N_('出现更加危险的敌人')] },
|
||||
ABYSS: { code: 'AC-4', desc: N_('敌方攻击强度到达极限的模拟训练'), effects: [N_('作战环境无比困难'), N_('出现极度危险的敌人')] },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,13 +43,17 @@ export const STAGE_POOL = { FUNNY: ['act1autochess_m01'], NORMAL: 8, HARD: 7, AB
|
||||
|
||||
/**
|
||||
* Display name of a stage: stages.json when it is loaded, else derived from the id (act1 m0N → 战场#0N, act2 m0N → 战场#0(N+4)).
|
||||
* From the data name only its 战场#NN part (with a (上半) / (下半) mark) — 'Battlefield #NN (First Half)' in English.
|
||||
* @param {string} id e.g. 'act1autochess_m01'
|
||||
*/
|
||||
export function stageLabel(id) {
|
||||
const rec = getStage(id);
|
||||
if (rec && typeof rec.name === 'string' && rec.name) return rec.name.split(/\s+/)[0];
|
||||
if (rec && typeof rec.name === 'string' && rec.name) {
|
||||
const m = rec.name.match(/^(.*?#\d+(?:\s*[((][^))]*[))])?)/);
|
||||
return m ? m[1] : rec.name.split(/\s+/)[0];
|
||||
}
|
||||
const m = String(id || '').match(/^act(\d)autochess_m(\d+)$/);
|
||||
return m ? `战场#${String(Number(m[2]) + (m[1] === '2' ? 4 : 0)).padStart(2, '0')}` : '';
|
||||
return m ? t('战场#{no}', { no: String(Number(m[2]) + (m[1] === '2' ? 4 : 0)).padStart(2, '0') }) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,22 +65,22 @@ export function stageLabel(id) {
|
||||
export function stageNote(stages) {
|
||||
if (Array.isArray(stages)) {
|
||||
const ids = stages.filter((s) => typeof s === 'string' && s);
|
||||
if (ids.length === 1) { const name = stageLabel(ids[0]); return name ? `战场固定为 ${name}` : '战场固定'; }
|
||||
return ids.length > 1 ? `战场随机(共${ids.length}张)` : '';
|
||||
if (ids.length === 1) { const name = stageLabel(ids[0]); return name ? t('战场固定为 {name}', { name }) : t('战场固定'); }
|
||||
return ids.length > 1 ? t('战场随机(共{n}张)', { n: ids.length }) : '';
|
||||
}
|
||||
return Number.isInteger(stages) && stages > 1 ? `战场随机(共${stages}张)` : '';
|
||||
return Number.isInteger(stages) && stages > 1 ? t('战场随机(共{stages}张)', { stages }) : '';
|
||||
}
|
||||
|
||||
const MODE_CARDS = [
|
||||
{
|
||||
id: 'solo', name: '独立模拟', en: 'SOLO SIMULATION', icon: 'user',
|
||||
desc: '独自调配资金与干员,以自己的节奏完成整场模拟。',
|
||||
points: ['1 名博士', '休整期与机变阶段不限时'],
|
||||
id: 'solo', name: N_('独立模拟'), en: 'SOLO SIMULATION', icon: 'user',
|
||||
desc: N_('独自调配资金与干员,以自己的节奏完成整场模拟。'),
|
||||
points: [N_('1 名博士'), N_('休整期与机变阶段不限时')],
|
||||
},
|
||||
{
|
||||
id: 'coop', name: '同盟模拟', en: 'ALLIANCE SIMULATION', icon: 'users',
|
||||
desc: `与至多 ${MAX_SEATS - 1} 名博士组成同盟,共享干员池,联防协作抵御敌潮。`,
|
||||
points: [`1–${MAX_SEATS} 名博士 · 可由 AI 队友补位`, '联防阶段 · 最终攻势合并生命值'],
|
||||
id: 'coop', name: N_('同盟模拟'), en: 'ALLIANCE SIMULATION', icon: 'users',
|
||||
desc: N_('与至多 {n} 名博士组成同盟,共享干员池,联防协作抵御敌潮。'), params: { n: MAX_SEATS - 1 },
|
||||
points: [N_('1–{n} 名博士 · 可由 AI 队友补位'), N_('联防阶段 · 最终攻势合并生命值')], pointParams: { n: MAX_SEATS },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -89,11 +96,11 @@ export function difficultyInfo(roomMode, difficulty) {
|
||||
const m = DIFFICULTIES.includes(difficulty) ? getMode(modeIdFor(roomMode, difficulty)) : null;
|
||||
const effects = Array.isArray(m?.effectDescList)
|
||||
? m.effectDescList.map((e) => String(e).replace(/^[·•\s]+/, '')).filter(Boolean)
|
||||
: fallback.effects;
|
||||
: fallback.effects.map((e) => t(e));
|
||||
const rounds = Number.isFinite(m?.lastRound) ? m.lastRound : roomMode === 'solo' && difficulty === 'FUNNY' ? 9 : 14;
|
||||
return {
|
||||
code: typeof m?.code === 'string' ? m.code : fallback.code,
|
||||
desc: typeof m?.desc === 'string' ? m.desc : fallback.desc,
|
||||
desc: typeof m?.desc === 'string' ? m.desc : t(fallback.desc),
|
||||
effects,
|
||||
rounds,
|
||||
hidden: difficulty !== 'FUNNY',
|
||||
@@ -160,20 +167,21 @@ export function rememberRoom(code) {
|
||||
}
|
||||
|
||||
const FALLBACK_TIPS = [
|
||||
'联合模拟在选择策略时可以进行一次跳过',
|
||||
'调度中心即使冻结,依然可以主动刷新',
|
||||
'两件同名装备可以合成一件更强力的装备',
|
||||
'只有达成完美作战的队友可以进行联防',
|
||||
N_('联合模拟在选择策略时可以进行一次跳过'),
|
||||
N_('调度中心即使冻结,依然可以主动刷新'),
|
||||
N_('两件同名装备可以合成一件更强力的装备'),
|
||||
N_('只有达成完美作战的队友可以进行联防'),
|
||||
];
|
||||
const TIP_ROTATE_MS = 5000; // matchingTipRotateInterval
|
||||
|
||||
/** Rotating tactical tips (config.json `tips`, weighted list of { tip, weight }). */
|
||||
function TipsPanel() {
|
||||
const cfg = getConfig();
|
||||
const fallback = FALLBACK_TIPS.map((x) => t(x));
|
||||
const tips = Array.isArray(cfg?.tips)
|
||||
? cfg.tips.map((t) => (typeof t === 'string' ? t : t?.tip)).filter((t) => typeof t === 'string' && t)
|
||||
: FALLBACK_TIPS;
|
||||
const list = tips.length ? tips : FALLBACK_TIPS;
|
||||
? cfg.tips.map((x) => (typeof x === 'string' ? x : x?.tip)).filter((x) => typeof x === 'string' && x)
|
||||
: fallback;
|
||||
const list = tips.length ? tips : fallback;
|
||||
const [idx, setIdx] = useState(() => Math.floor(Math.random() * list.length));
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setIdx((i) => i + 1), TIP_ROTATE_MS);
|
||||
@@ -183,11 +191,11 @@ function TipsPanel() {
|
||||
return html`<div class="tips brackets">
|
||||
<div class="tips__head">
|
||||
<${Icon} name="info" />
|
||||
<span>作战提示</span>
|
||||
<span>${t('作战提示')}</span>
|
||||
<${MicroLabel}>TACTICAL TIPS<//>
|
||||
<span class="tips__idx num">${String(i + 1).padStart(2, '0')}<span class="t-dim">/${String(list.length).padStart(2, '0')}</span></span>
|
||||
<button type="button" class="tips__nav" onClick=${() => setIdx(i - 1 + list.length)} aria-label="上一条"><${Icon} name="chevronLeft" /></button>
|
||||
<button type="button" class="tips__nav" onClick=${() => setIdx(i + 1)} aria-label="下一条"><${Icon} name="chevronRight" /></button>
|
||||
<button type="button" class="tips__nav" onClick=${() => setIdx(i - 1 + list.length)} aria-label=${t('上一条')}><${Icon} name="chevronLeft" /></button>
|
||||
<button type="button" class="tips__nav" onClick=${() => setIdx(i + 1)} aria-label=${t('下一条')}><${Icon} name="chevronRight" /></button>
|
||||
</div>
|
||||
<p key=${i} class="tips__text">${list[i]}</p>
|
||||
</div>`;
|
||||
@@ -200,11 +208,11 @@ function ModeCard({ card, selected, onSelect }) {
|
||||
<span class="mode-card__icon"><${Icon} name=${card.icon} /></span>
|
||||
<span class="mode-card__text">
|
||||
<${MicroLabel} tone=${selected ? 'mint' : undefined}>${card.en}<//>
|
||||
<span class="mode-card__name">${card.name}</span>
|
||||
<span class="mode-card__desc">${card.desc}</span>
|
||||
<span class="mode-card__points">${card.points.map((p) => html`<span key=${p}>${p}</span>`)}</span>
|
||||
<span class="mode-card__name">${t(card.name)}</span>
|
||||
<span class="mode-card__desc">${t(card.desc, card.params)}</span>
|
||||
<span class="mode-card__points">${card.points.map((p) => html`<span key=${p}>${t(p, card.pointParams)}</span>`)}</span>
|
||||
</span>
|
||||
<span class="mode-card__check" aria-hidden="true"><${Icon} name="check" />已选定</span>
|
||||
<span class="mode-card__check" aria-hidden="true"><${Icon} name="check" />${t('已选定')}</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
@@ -215,15 +223,15 @@ function DifficultyCard({ roomMode, difficulty, selected, onSelect }) {
|
||||
<span class="diff-card__bar" aria-hidden="true"></span>
|
||||
<span class="diff-card__head">
|
||||
<${DifficultyIcon} difficulty=${difficulty} class="diff-card__glyph" />
|
||||
<span class="diff-card__name">${DIFFICULTY_NAMES[difficulty]}</span>
|
||||
<span class="diff-card__name">${t(DIFFICULTY_NAMES[difficulty])}</span>
|
||||
<span class="diff-card__code num">${info.code}</span>
|
||||
<span class="diff-card__meta">
|
||||
<span class="num">${info.rounds}</span> 回合${info.hidden ? html`<span class="diff-card__hidden">+ 隐秘核心</span>` : null}
|
||||
<span class="num">${info.rounds}</span> ${tc('rounds', '回合')}${info.hidden ? html`<span class="diff-card__hidden">${t('+ 隐秘核心')}</span>` : null}
|
||||
</span>
|
||||
</span>
|
||||
<span class="diff-card__desc">${info.desc}</span>
|
||||
<span class="diff-card__effects">${info.effects.map((e) => html`<span key=${e}>${e}</span>`)}${info.stageNote ? html`<span key="stage" class="diff-card__stage"><${Icon} name="rook" />${info.stageNote}</span>` : null}</span>
|
||||
<span class="diff-card__check" aria-hidden="true"><${Icon} name="check" /><span>已选定</span></span>
|
||||
<span class="diff-card__check" aria-hidden="true"><${Icon} name="check" /><span>${t('已选定')}</span></span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
@@ -252,7 +260,7 @@ export function LobbyScreen() {
|
||||
|
||||
const run = async (kind, fn) => {
|
||||
if (inFlight.current) return;
|
||||
if (!online) { toast('尚未连接到服务器,请稍候', 'warn'); return; }
|
||||
if (!online) { toast(t('尚未连接到服务器,请稍候'), 'warn'); return; }
|
||||
inFlight.current = true;
|
||||
setBusy(kind);
|
||||
try { await fn(); } catch (err) { toastError(err); } finally {
|
||||
@@ -265,23 +273,23 @@ export function LobbyScreen() {
|
||||
// `onClick=${join}` hands the click EVENT as the first argument, and a default parameter only applies to
|
||||
// `undefined` — codeArg keeps an event target out of the key and falls back to the input field
|
||||
const k = codeArg(c, code);
|
||||
if (!k) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
|
||||
if (!k) { toast(t('同盟密钥为 {ROOM_CODE_LEN} 位字母或数字', { ROOM_CODE_LEN }), 'warn'); return; }
|
||||
run('join', () => net.request('room.join', { code: k }));
|
||||
};
|
||||
// a spectator seat: no player seat taken, nothing to do but watch (also a match already running)
|
||||
const spectate = (c = code) => {
|
||||
// same guard as join: `onClick=${spectate}` passes the click event, not a code
|
||||
const k = codeArg(c, code);
|
||||
if (!k) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
|
||||
if (!k) { toast(t('同盟密钥为 {ROOM_CODE_LEN} 位字母或数字', { ROOM_CODE_LEN }), 'warn'); return; }
|
||||
run('spectate', () => net.request('room.spectate', { code: k }).catch((err) => {
|
||||
// Clearer than the bare ERR_TEXT: the usual cause is a code that is not the host's (a remembered one from an
|
||||
// earlier room, or another machine's) — the server can only answer "no such room".
|
||||
if (err?.code === ERR.ROOM_NOT_FOUND) {
|
||||
toast(`没有找到密钥 ${k} 对应的同盟:请和房主核对密钥(同盟结束后密钥即失效)`, 'warn');
|
||||
toast(t('没有找到密钥 {k} 对应的同盟:请和房主核对密钥(同盟结束后密钥即失效)', { k }), 'warn');
|
||||
return;
|
||||
}
|
||||
if (err?.code === ERR.ALREADY) {
|
||||
toast('你已经是该同盟的博士:先离开同盟,才能以观战身份进入', 'warn');
|
||||
toast(t('你已经是该同盟的博士:先离开同盟,才能以观战身份进入'), 'warn');
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
@@ -295,20 +303,20 @@ export function LobbyScreen() {
|
||||
return html`<div class="screen lobby-screen">
|
||||
<header class="topbar">
|
||||
<div class="topbar__left">
|
||||
<${Button} variant="ghost" size="sm" icon="chevronLeft" onClick=${backToTitle} title="返回标题">返回<//>
|
||||
<${Button} variant="ghost" size="sm" icon="chevronLeft" onClick=${backToTitle} title=${t('返回标题')}>${t('返回')}<//>
|
||||
<${PingPill} ms=${conn.ping} online=${online} />
|
||||
</div>
|
||||
<div class="topbar__center">
|
||||
<${MicroLabel} tone="mint">SIMULATION PROTOCOL SELECT<//>
|
||||
<h1 class="topbar__title">选择模拟协议</h1>
|
||||
<h1 class="topbar__title">${t('选择模拟协议')}</h1>
|
||||
</div>
|
||||
<div class="topbar__right">
|
||||
<${GuideButton} class="lobby-guide" variant="secondary" />
|
||||
<${LoadoutButton} from="lobby" size="sm" class="lobby-loadout" />
|
||||
<${GuideButton} class="lobby-guide" variant="secondary" label=${t('玩法说明')} />
|
||||
<${LoadoutButton} from="lobby" size="sm" class="lobby-loadout" label=${t('干员调配')} />
|
||||
<div class="me-chip">
|
||||
<${AvatarFrame} size="sm" name=${me.name} seat=${0} self=${true} />
|
||||
<div class="me-chip__text">
|
||||
<span class="me-chip__name">${me.name || '博士'}</span>
|
||||
<span class="me-chip__name">${me.name || t('博士')}</span>
|
||||
<${MicroLabel}>${me.playerId != null ? `DOCTOR #${doctorNo(me.playerId)}` : 'DOCTOR'}<//>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,45 +325,45 @@ export function LobbyScreen() {
|
||||
|
||||
<div class="lobby-body screen__scroll">
|
||||
<section class="lobby-left">
|
||||
<div class="section-label"><span class="section-label__idx num">01</span>模拟方式<${MicroLabel}>MODE<//></div>
|
||||
<div class="section-label"><span class="section-label__idx num">01</span>${t('模拟方式')}<${MicroLabel}>MODE<//></div>
|
||||
<div class="mode-cards">
|
||||
${MODE_CARDS.map((c) => html`<${ModeCard} key=${c.id} card=${c} selected=${roomMode === c.id} onSelect=${pickMode} />`)}
|
||||
</div>
|
||||
|
||||
<div class="section-label"><span class="section-label__idx num">03</span>加入同盟<${MicroLabel}>JOIN WITH ALLIANCE KEY<//></div>
|
||||
<div class="section-label"><span class="section-label__idx num">03</span>${t('加入同盟')}<${MicroLabel}>JOIN WITH ALLIANCE KEY<//></div>
|
||||
<${Panel} class="join-panel" tone="amber">
|
||||
<div class="join-row">
|
||||
<${TextField} size="code" icon="key" value=${code} placeholder="输入同盟密钥 / 粘贴邀请链接"
|
||||
<${TextField} size="code" icon="key" value=${code} placeholder=${t('输入同盟密钥 / 粘贴邀请链接')}
|
||||
transform=${normalizeCode} onInput=${(v) => setCode(normalizeCode(v))} onEnter=${() => join()} />
|
||||
<${Button} variant="amber" size="lg" icon="users" loading=${busy === 'join'} disabled=${!codeOk || !online} onClick=${() => join()}>加入同盟<//>
|
||||
<${Tooltip} text=${`以观战者身份进入:不占博士席位,只能观看(每个同盟最多 ${MAX_SPECTATORS} 名,模拟进行中也可进入)`}>
|
||||
<${Button} variant="secondary" size="lg" icon="eye" class="join-spectate" loading=${busy === 'spectate'} disabled=${!codeOk || !online} onClick=${spectate}>观战<//>
|
||||
<${Button} variant="amber" size="lg" icon="users" loading=${busy === 'join'} disabled=${!codeOk || !online} onClick=${() => join()}>${t('加入同盟')}<//>
|
||||
<${Tooltip} text=${t('以观战者身份进入:不占博士席位,只能观看(每个同盟最多 {MAX_SPECTATORS} 名,模拟进行中也可进入)', { MAX_SPECTATORS })}>
|
||||
<${Button} variant="secondary" size="lg" icon="eye" class="join-spectate" loading=${busy === 'spectate'} disabled=${!codeOk || !online} onClick=${spectate}>${t('观战')}<//>
|
||||
<//>
|
||||
</div>
|
||||
<div class="join-foot">
|
||||
${recent.length ? html`<span class="t-lo">最近的同盟</span>
|
||||
${recent.map((c) => html`<button key=${c} type="button" class="code-chip num" title="填入密钥(不会直接加入)"
|
||||
${recent.length ? html`<span class="t-lo">${t('最近的同盟')}</span>
|
||||
${recent.map((c) => html`<button key=${c} type="button" class="code-chip num" title=${t('填入密钥(不会直接加入)')}
|
||||
onClick=${() => setCode(c)}>${c}</button>`)}`
|
||||
: html`<span class="t-dim">向同伴索取 ${ROOM_CODE_LEN} 位同盟密钥,或直接打开邀请链接</span>`}
|
||||
: html`<span class="t-dim">${t('向同伴索取 {ROOM_CODE_LEN} 位同盟密钥,或直接打开邀请链接', { ROOM_CODE_LEN })}</span>`}
|
||||
</div>
|
||||
<//>
|
||||
<${TipsPanel} />
|
||||
</section>
|
||||
|
||||
<section class="lobby-right">
|
||||
<div class="section-label"><span class="section-label__idx num">02</span>模拟难度<${MicroLabel}>DIFFICULTY<//></div>
|
||||
<div class="section-label"><span class="section-label__idx num">02</span>${t('模拟难度')}<${MicroLabel}>DIFFICULTY<//></div>
|
||||
<div class="diff-list">
|
||||
${DIFFICULTIES.map((d) => html`<${DifficultyCard} key=${d} roomMode=${roomMode} difficulty=${d} selected=${difficulty === d} onSelect=${pickDifficulty} />`)}
|
||||
</div>
|
||||
<div class="create-box">
|
||||
<${Tooltip} block=${true} text=${online ? null : '正在连接服务器…'}>
|
||||
<${Tooltip} block=${true} text=${online ? null : t('正在连接服务器…')}>
|
||||
<${Button} variant="primary" size="xl" block=${true} iconRight="chevrons" loading=${busy === 'create'} disabled=${!online} onClick=${create}>
|
||||
${roomMode === 'solo' ? '开始独立模拟' : '创建同盟'}
|
||||
${roomMode === 'solo' ? t('开始独立模拟') : t('创建同盟')}
|
||||
<//>
|
||||
<//>
|
||||
<div class="create-box__hint">
|
||||
${online
|
||||
? html`<span>${roomMode === 'solo' ? '创建后即可开始模拟' : '创建后可邀请好友或添加 AI 队友'}</span>`
|
||||
? html`<span>${roomMode === 'solo' ? t('创建后即可开始模拟') : t('创建后可邀请好友或添加 AI 队友')}</span>`
|
||||
: html`<${Spinner} size="sm" label="CONNECTING" />`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user