feedback5 W5S: i18n phase 2, ui game logic — phase banners and capsule labels, placement and shop refusal reasons, reward offer headers, the ready prompt, watch / view-switcher labels, statuses, bond tips through t(); fmtNum writes K / M / B in English (万 / 亿 count in 10⁴ steps); English for the 6 new msgids; Chinese output unchanged (zh render proof; two rewrites checked by hand)

This commit is contained in:
sganggs committed 2026-10-06 18:13:55 +08:00
1 parent 647bd23ef2
commit 5741dc129b
10 files changed
+108 -85

No files matched your search

+7 -1
View File
@@ -1069,5 +1069,11 @@
"撤退[Q]": "Retreat [Q]",
"出售(+{sell} 资金,X)": "Sell (+{sell} Funds, X)",
"出售[X]": "Sell [X]",
"临时整备区 {n} 件道具待处理": "Temporary Bench: {n} {count|item|items} pending"
"临时整备区 {n} 件道具待处理": "Temporary Bench: {n} {count|item|items} pending",
"{name}:部分盟约所含干员阵容不完整({bannedN} 名干员无法出现)": "{name}: some of the Alliance's Operators cannot appear ({bannedN} {bannedN|Operator|Operators} banned)",
"{name}:部分盟约所含干员阵容不完整": "{name}: some of the Alliance's Operators cannot appear",
"联防:{names}": "Unite: {names}",
"剩余资金": "Funds Left",
"继续整备": "Keep Preparing",
"还有 {funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?": "You still have {funds} unspent Funds. The round's leftover Funds are cleared when the Rest Phase ends. Ready anyway?"
}
+4 -3
View File
@@ -1,6 +1,7 @@
// ui/gameLogic/bonds.js — bond counts, members, harmony, 本局禁用. Re-exported from ../gameLogic.js.
import { int, isObj } from './shared.js';
import { t } from '../../../../shared/i18n.js';
// ---- bonds -------------------------------------------------------------------------------------------
@@ -275,8 +276,8 @@ export function disabledBondSets(pub, staticInactive = []) {
* @param {number} bannedN banned operators of the bond
*/
export function briefingBondTip(name, state, bannedN = 0) {
if (state === 'off') return `${name}:本局禁用(该盟约不会激活)`;
if (state === 'drawn' || bannedN > 0) return `${name}:部分盟约所含干员阵容不完整${bannedN ? `(${bannedN} 名干员无法出现)` : ''}`;
if (state === 'off') return t('{name}:本局禁用(该盟约不会激活)', { name });
if (state === 'drawn' || bannedN > 0) return bannedN ? t('{name}:部分盟约所含干员阵容不完整({bannedN} 名干员无法出现)', { name, bannedN }) : t('{name}:部分盟约所含干员阵容不完整', { name });
return name;
}
@@ -314,5 +315,5 @@ export function bandOffBonds(band, off) {
*/
export function bandOffLine(names) {
const list = (Array.isArray(names) ? names : []).filter((n) => typeof n === 'string' && n);
return list.length ? `本局禁用${list.map((n) => `【${n}】`).join('')}盟约,此策略效果可能无法发挥` : '';
return list.length ? t('本局禁用{names}盟约,此策略效果可能无法发挥', { names: list.map((n) => t('【{name}】', { name: n })).join('') }) : '';
}
+21 -11
View File
@@ -2,20 +2,21 @@
import { UF } from '../../../../shared/constants.js';
import { clamp, isObj, tileKey } from './shared.js';
import { N_, getLang } from '../../../../shared/i18n.js';
// ---- players, statuses, fields ------------------------------------------------------------------------
/** Status → glyph + text (research 06 §11.1). */
export const STATUS_META = Object.freeze({
acting: { glyph: 'dots', text: '行动中', tone: 'lo' },
ready: { glyph: 'check', text: '已就绪', tone: 'mint' },
deciding: { glyph: 'hourglass', text: '决策中', tone: 'gold' },
combat: { glyph: 'sword', text: '作战中', tone: 'orange' },
done: { glyph: 'check', text: '作战结束', tone: 'mint' },
helping: { glyph: 'shield', text: '联防中', tone: 'orange' },
left: { glyph: 'exit', text: '已离开', tone: 'red' },
dead: { glyph: 'close', text: '已淘汰', tone: 'red' },
acting: { glyph: 'dots', text: N_('行动中'), tone: 'lo' },
ready: { glyph: 'check', text: N_('已就绪'), tone: 'mint' },
deciding: { glyph: 'hourglass', text: N_('决策中'), tone: 'gold' },
combat: { glyph: 'sword', text: N_('作战中'), tone: 'orange' },
done: { glyph: 'check', text: N_('作战结束'), tone: 'mint' },
helping: { glyph: 'shield', text: N_('联防中'), tone: 'orange' },
left: { glyph: 'exit', text: N_('已离开'), tone: 'red' },
dead: { glyph: 'close', text: N_('已淘汰'), tone: 'red' },
});
// ---- snapshots / battle HUD ---------------------------------------------------------------------------------
@@ -69,12 +70,21 @@ export function attackInterval(bat, aspd = 100) {
return b * 100 / a;
}
/** Compact number: 12345 → '12,345'; 1.5e6 → '150万'. */
/**
* Compact number: 12345 → '12,345'; 1.5e6 → '150万' — in English the thousands-based units: 1.5e6 → '1.5M', 2.5e5 →
* '250K', 3e9 → '3B' (万 / 亿 count in 10⁴ / 10⁸ steps, no template can move the decimal point).
*/
export function fmtNum(v) {
const n = Number(v);
if (!Number.isFinite(n)) return '—';
if (Math.abs(n) >= 1e8) return `${(n / 1e8).toFixed(n >= 1e9 ? 0 : 1)}亿`;
if (Math.abs(n) >= 1e5) return `${(n / 1e4).toFixed(n >= 1e6 ? 0 : 1)}万`;
if (getLang() === 'en') {
if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(Math.abs(n) >= 1e10 ? 0 : 1)}B`;
if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(Math.abs(n) >= 1e7 ? 0 : 1)}M`;
if (Math.abs(n) >= 1e5) return `${Math.round(n / 1e3)}K`;
return Math.round(n).toLocaleString('en-US');
}
if (Math.abs(n) >= 1e8) return `${(n / 1e8).toFixed(n >= 1e9 ? 0 : 1)}亿`; // i18n-ignore: the Chinese units (English above)
if (Math.abs(n) >= 1e5) return `${(n / 1e4).toFixed(n >= 1e6 ? 0 : 1)}万`; // i18n-ignore
return Math.round(n).toLocaleString('en-US');
}
+2 -1
View File
@@ -3,6 +3,7 @@
import { MODULE_NONE, loadoutOptions, resolveLoadout } from '../../../../shared/protocol.js';
import { loadoutRecord, resolveRecordLoadout } from '../../../../shared/loadoutRecord.js';
import { isObj } from './shared.js';
import { t } from '../../../../shared/i18n.js';
// ---- operator loadout (DESIGN §16) ----------------------------------------------------------------------------------
@@ -34,7 +35,7 @@ export function chessLoadout(chess, loadout, getChess = () => null) {
let defaultModule = true;
if (chess.isGolden) {
const id = r.moduleId ?? (chess.module?.active ? chess.module.id : MODULE_NONE);
if (id === MODULE_NONE) module = { id: MODULE_NONE, name: '未装备模组', typeName: '', none: true };
if (id === MODULE_NONE) module = { id: MODULE_NONE, name: t('未装备模组'), typeName: '', none: true };
else {
const rec = (Array.isArray(chess.modules) ? chess.modules : []).find((m) => isObj(m) && m.uniEquipId === id)
|| (isObj(chess.module) && chess.module.id === id ? { uniEquipId: id, name: chess.module.name, typeName: chess.module.type } : null);
+15 -14
View File
@@ -4,6 +4,7 @@ import { PHASE } from '../../../../shared/constants.js';
import { bossLevelSeconds } from '../matchStatus.js';
import { clamp, int, isObj, sortedPlayers } from './shared.js';
import { normalizeSp } from './draft.js';
import { t } from '../../../../shared/i18n.js';
// ---- phases ----------------------------------------------------------------------------------------
@@ -43,18 +44,18 @@ export const isBossPhase = (phase) => phase === PHASE.FINAL_ASSAULT || phase ===
export function phaseBanner(phase, pub) {
const r = int(pub?.round, 0);
switch (phase) {
case PHASE.BATTLE_CHECK: return { title: '协议启动', micro: 'PROTOCOL START', tone: 'mint', sub: '模拟即将开始', duration: 2600 };
case PHASE.ROUND_START: return { title: `第 ${r} 回合`, micro: `ROUND ${String(r).padStart(2, '0')}`, tone: 'mint', sub: '资金已到账' };
case PHASE.SP_DRAFT: return { title: '机变阶段', micro: 'CONTINGENCY', tone: 'gold', sub: '依次选择机变' };
case PHASE.PREP: return { title: '休整期', micro: `ROUND ${String(r).padStart(2, '0')} // REST`, tone: 'mint', sub: '部署干员,准备迎敌' };
case PHASE.COMBAT: return { title: '作战开始', micro: 'COMBAT', tone: 'orange', sub: '各自行动阶段' };
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('资金已到账') };
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('各自行动阶段') };
case PHASE.UNITE: {
const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || '博士']));
const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || t('博士')]));
const helpers = Array.isArray(pub?.unite?.helpers) ? pub.unite.helpers.map((id) => names.get(id)).filter(Boolean) : [];
return { title: '联防阶段', micro: 'JOINT DEFENSE', tone: 'orange', sub: helpers.length ? `联防:${helpers.join('、')}` : '完美作战的博士迎战突破防线的敌人' };
return { title: t('联防阶段'), micro: 'JOINT DEFENSE', tone: 'orange', sub: helpers.length ? t('联防:{names}', { names: helpers }) : t('完美作战的博士迎战突破防线的敌人') };
}
case PHASE.FINAL_ASSAULT: return { title: '最终攻势', micro: 'FINAL ASSAULT', tone: 'red', sub: '击败敌方领袖' };
case PHASE.HIDDEN_CORE: return { title: '隐秘核心', micro: 'HIDDEN CORE', tone: 'red', sub: '被源石侵蚀的假想敌' };
case PHASE.FINAL_ASSAULT: return { title: t('最终攻势'), micro: 'FINAL ASSAULT', tone: 'red', sub: t('击败敌方领袖') };
case PHASE.HIDDEN_CORE: return { title: t('隐秘核心'), micro: 'HIDDEN CORE', tone: 'red', sub: t('被源石侵蚀的假想敌') };
case PHASE.SETTLE: return null;
default: return null;
}
@@ -62,11 +63,11 @@ export function phaseBanner(phase, pub) {
/** Label of the prep capsule ("休息一下" in the original). */
export function prepCapsuleLabel(phase) {
if (phase === PHASE.SP_DRAFT) return '机变阶段';
if (phase === PHASE.ROUND_START) return '回合开始';
if (phase === PHASE.BATTLE_CHECK) return '协议启动';
if (phase === PHASE.SETTLE) return '回合结算';
return '休息一下';
if (phase === PHASE.SP_DRAFT) return t('机变阶段');
if (phase === PHASE.ROUND_START) return t('回合开始');
if (phase === PHASE.BATTLE_CHECK) return t('协议启动');
if (phase === PHASE.SETTLE) return t('回合结算');
return t('休息一下');
}
// ---- countdown -------------------------------------------------------------------------------------
+25 -24
View File
@@ -7,6 +7,7 @@ import { pieceDir, rangeTiles } from '../facing.js';
import { isObj, tileKey } from './shared.js';
import { boardTileOf, fieldTile } from './camera.js';
import { deployedRecord, fieldsStandIn, standInOf } from './standIn.js';
import { t } from '../../../../shared/i18n.js';
// ---- placement (canPlace mirror) ------------------------------------------------------------------------
@@ -310,45 +311,45 @@ function unitAllowed(ctx, piece, row, col, owner = null) {
*/
export function canPlace(ctx, uid, target) {
const no = (code, reason) => ({ ok: false, code, reason });
if (!ctx || !ctx.editable) return no('WRONG_PHASE', '当前阶段无法进行该操作');
if (!ctx || !ctx.editable) return no('WRONG_PHASE', t('当前阶段无法进行该操作'));
const src = ctx.pieces.get(uid);
if (!src) return no('BAD_TARGET', '找不到该单位');
if (!isObj(target)) return no('BAD_TILE', '无法部署在该位置');
if (!src) return no('BAD_TARGET', t('找不到该单位'));
if (!isObj(target)) return no('BAD_TILE', t('无法部署在该位置'));
const piece = src.piece;
const isMagic = piece.kind === 'item' && ctx.getItem(piece.id)?.itemType === 'MAGIC';
if (target.area === 'hand') {
const idx = target.idx;
if (!Number.isInteger(idx) || idx < 0 || idx >= GEO.HAND_SIZE) return no('BAD_TILE', '无法放置在该位置');
if (src.area === 'hand' && src.idx === idx) return no('ALREADY', '位置未变化');
if (!Number.isInteger(idx) || idx < 0 || idx >= GEO.HAND_SIZE) return no('BAD_TILE', t('无法放置在该位置'));
if (src.area === 'hand' && src.idx === idx) return no('ALREADY', t('位置未变化'));
const occ = ctx.handAt.get(idx);
if (!occ) return { ok: true, action: 'move' };
if (piece.kind === 'item') {
if (occ.piece.kind === 'chess') return isMagic ? no('BAD_TARGET', '该道具需要放置在战场上使用') : equipCheck(ctx, piece, occ.piece);
if (occ.piece.kind === 'chess') return isMagic ? no('BAD_TARGET', t('该道具需要放置在战场上使用')) : equipCheck(ctx, piece, occ.piece);
return { ok: true, action: 'swap' };
}
if (src.area === 'board') {
if (piece.kind === 'token') return { ok: true, action: 'move' }; // back onto its stack
if (occ.piece.kind === 'chess') {
if (!tileAllows(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', '交换后的单位无法部署在原位置');
if (!tileAllows(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', t('交换后的单位无法部署在原位置'));
return { ok: true, action: 'swap' };
}
// the withdrawn operator goes to another free slot
const free = [...Array(GEO.HAND_SIZE).keys()].some((i) => !ctx.handAt.has(i));
return free ? { ok: true, action: 'move' } : no('HAND_FULL', '整备区已满');
return free ? { ok: true, action: 'move' } : no('HAND_FULL', t('整备区已满'));
}
return { ok: true, action: 'swap' };
}
if (target.area === 'board') {
const { row, col } = target;
if (!Number.isInteger(row) || !Number.isInteger(col)) return no('BAD_TILE', '无法部署在该位置');
if (!Number.isInteger(row) || !Number.isInteger(col)) return no('BAD_TILE', t('无法部署在该位置'));
const inField = row >= GEO.FIELD.r0 && row <= GEO.FIELD.r1 && col >= GEO.FIELD.c0 && col <= GEO.FIELD.c1;
if (!inField) return no('BAD_TILE', '无法部署在该位置');
if (!inField) return no('BAD_TILE', t('无法部署在该位置'));
const occ = ctx.boardAt.get(tileKey(row, col));
if (piece.kind === 'item') {
if (isMagic) return { ok: true, action: 'art' };
if (!occ) return no('BAD_TARGET', '请将装备拖拽至干员身上');
if (!occ) return no('BAD_TARGET', t('请将装备拖拽至干员身上'));
return equipCheck(ctx, piece, occ.piece);
}
// its own tile: re-orient in place through the direction wheel (research 09 §1.2)
@@ -356,38 +357,38 @@ export function canPlace(ctx, uid, target) {
if (!tileAllows(ctx, piece, row, col)) {
const deployable = ctx.deploy.ranged.has(tileKey(row, col));
const pos = piecePosition(ctx, piece);
return no('BAD_TILE', deployable && pos === 'MELEE' ? '近战单位只能部署在地面' : deployable && pos === 'HIGH' ? '只能部署在远程位' : '无法部署在该位置');
return no('BAD_TILE', deployable && pos === 'MELEE' ? t('近战单位只能部署在地面') : deployable && pos === 'HIGH' ? t('只能部署在远程位') : t('无法部署在该位置'));
}
// a range-bound summon (战术点): inside its owner's attack range — seen from the summon's old tile when it is
// dropped onto its own owner (the two swap); an outside-bound one (战术锚点) outside it
const ownerSwap = src.area === 'board' && occ && occ.piece.uid === piece.ownerUid ? { row: src.row, col: src.col, piece: occ.piece } : null;
const range = summonRange(ctx, piece, ownerSwap);
if (range && !range.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围内');
if (range && !range.has(tileKey(row, col))) return no('BAD_TILE', t('只能部署在召唤者攻击范围内'));
const out = summonExcluded(ctx, piece, ownerSwap);
if (out && out.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围外');
if (out && out.has(tileKey(row, col))) return no('BAD_TILE', t('只能部署在召唤者攻击范围外'));
if (src.area === 'board') {
// board → board: move or swap (the occupant must be legal on the source tile — the mover's own summon excepted:
// a moved operator's summons go back to the hand anyway)
const ownSummon = occ && occ.piece.kind === 'token' && occ.piece.ownerUid === piece.uid;
if (occ && !ownSummon && !unitAllowed(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', '交换后的单位无法部署在原位置');
if (occ && !ownSummon && !unitAllowed(ctx, occ.piece, src.row, src.col)) return no('BAD_TILE', t('交换后的单位无法部署在原位置'));
return { ok: true, action: occ ? 'swap' : 'move' };
}
if (piece.kind === 'token') {
if (occ) return no('BAD_TILE', '该位置已有单位');
if (occ) return no('BAD_TILE', t('该位置已有单位'));
const ownerDeployed = [...ctx.boardAt.values()].some((e) => e.piece.uid === piece.ownerUid);
if (Number.isInteger(piece.ownerUid) && !ownerDeployed) return no('BAD_TARGET', '召唤者尚未部署');
if (Number.isInteger(piece.ownerUid) && !ownerDeployed) return no('BAD_TARGET', t('召唤者尚未部署'));
return { ok: true, action: 'move' };
}
if ((!occ || occ.piece.kind !== 'chess') && ctx.count >= ctx.cap) return no('BOARD_FULL', '已达到部署上限');
if ((!occ || occ.piece.kind !== 'chess') && ctx.count >= ctx.cap) return no('BOARD_FULL', t('已达到部署上限'));
return { ok: true, action: occ ? 'swap' : 'move' };
}
return no('BAD_TILE', '无法放置在该位置');
return no('BAD_TILE', t('无法放置在该位置'));
}
function equipCheck(ctx, itemPiece, targetPiece) {
const item = ctx.getItem(itemPiece.id);
if (item?.itemType === 'MAGIC') return { ok: false, code: 'BAD_TARGET', reason: '该道具需要放置在战场上使用' };
if (!isObj(targetPiece) || targetPiece.kind !== 'chess') return { ok: false, code: 'BAD_TARGET', reason: '装备只能配发给干员' };
if (item?.itemType === 'MAGIC') return { ok: false, code: 'BAD_TARGET', reason: t('该道具需要放置在战场上使用') };
if (!isObj(targetPiece) || targetPiece.kind !== 'chess') return { ok: false, code: 'BAD_TARGET', reason: t('装备只能配发给干员') };
return { ok: true, action: 'equip' };
}
@@ -477,10 +478,10 @@ export function dropFailureReason(ctx, uid, tile) {
let target = null;
if (tile.area === 'board') target = { area: 'board', row: tile.row, col: tile.col };
else if (tile.area === 'hand') target = { area: 'hand', idx: Number.isInteger(tile.idx) ? tile.idx : tile.col };
else if (tile.area === 'temp') return ctx.pieces.get(uid).area === 'temp' ? null : '临时整备区无法放入单位';
else if (Number.isInteger(tile.row) && tile.row >= GEO.FIELD.r0 && tile.row <= GEO.FIELD.r1) return '无法部署在该位置';
else if (tile.area === 'temp') return ctx.pieces.get(uid).area === 'temp' ? null : t('临时整备区无法放入单位');
else if (Number.isInteger(tile.row) && tile.row >= GEO.FIELD.r0 && tile.row <= GEO.FIELD.r1) return t('无法部署在该位置');
else return null;
const res = canPlace(ctx, uid, target);
if (res.ok || res.code === 'ALREADY') return null;
return res.reason || '无法放置在该位置';
return res.reason || t('无法放置在该位置');
}
+2 -1
View File
@@ -1,6 +1,7 @@
// ui/gameLogic/result.js — end-of-match result normalisation. Re-exported from ../gameLogic.js.
import { int, isObj, sortedPlayers } from './shared.js';
import { t } from '../../../../shared/i18n.js';
// ---- result -----------------------------------------------------------------------------------------------------------
@@ -24,7 +25,7 @@ export function normalizeResult(res, pub) {
return {
playerId: p.playerId,
seat: int(p.seat, int(pp.seat, 0)),
name: p.name || pp.name || '博士',
name: p.name || pp.name || t('博士'),
isBot: !!(p.isBot ?? pp.isBot),
alive,
lp: teamLp != null ? (alive === false ? 0 : Math.max(0, teamLp)) : ownLp,
+18 -17
View File
@@ -2,6 +2,7 @@
import { GEO } from '../../../../shared/constants.js';
import { isObj } from './shared.js';
import { t } from '../../../../shared/i18n.js';
// ---- shop ---------------------------------------------------------------------------------------------
@@ -120,12 +121,12 @@ export function offerHeader(offer) {
const items = slots.some((s) => isObj(s) && s.kind === 'item');
const label = isObj(offer) && typeof offer.label === 'string' && offer.label ? offer.label : null;
const queued = isObj(offer) && Number.isInteger(offer.queued) && offer.queued > 0 ? offer.queued : 0;
const tail = { queued, more: queued ? `之后还有 ${queued} 项` : null };
const tail = { queued, more: queued ? t('之后还有 {queued} 项', { queued }) : null };
if (!items && (!isObj(offer) || offer.source === 'merge' || offer.source == null) && !label) {
return { title: '晋升奖励', micro: 'PROMOTION', sub: '免费选择 1 名', icon: 'crown', items: false, pill: '晋升奖励待选择', ...tail };
return { title: t('晋升奖励'), micro: 'PROMOTION', sub: t('免费选择 1 名'), icon: 'crown', items: false, pill: t('晋升奖励待选择'), ...tail };
}
const title = label || (items ? '装备补给' : '特殊招募');
return { title, micro: 'SPECIAL', sub: items ? '免费选择 1 件' : '免费选择 1 名', icon: 'refresh', items, pill: `${title}待选择`, ...tail };
const title = label || (items ? t('装备补给') : t('特殊招募'));
return { title, micro: 'SPECIAL', sub: items ? t('免费选择 1 件') : t('免费选择 1 名'), icon: 'refresh', items, pill: t('{title}待选择', { title }), ...tail };
}
/**
@@ -135,26 +136,26 @@ export function offerHeader(offer) {
* @returns {string|null} Chinese reason
*/
export function shopBlockReason(kind, { priv, editable, slot } = {}) {
if (!priv) return '尚未就绪';
if (priv.alive === false) return '你已被淘汰';
if (kind === 'ready') return priv.canReady === false ? '临时整备区不为空,请先处理溢出的资源' : null;
if (!priv) return t('尚未就绪');
if (priv.alive === false) return t('你已被淘汰');
if (kind === 'ready') return priv.canReady === false ? t('临时整备区不为空,请先处理溢出的资源') : null;
if (!editable) {
if (kind === 'reward') return '当前无法选择';
return priv.ready ? '已准备就绪,取消准备后才能操作' : '当前阶段无法进行该操作';
if (kind === 'reward') return t('当前无法选择');
return priv.ready ? t('已准备就绪,取消准备后才能操作') : t('当前阶段无法进行该操作');
}
const funds = Number(priv.funds) || 0;
const shop = priv.shop || {};
if (kind === 'buy' || kind === 'reward') {
if (!isObj(slot) || slot.sold) return kind === 'reward' ? '已选择' : '已售出';
if ((Number(slot.price) || 0) > funds) return '资金不足';
if (!isObj(slot) || slot.sold) return kind === 'reward' ? t('已选择') : t('已售出');
if ((Number(slot.price) || 0) > funds) return t('资金不足');
// a full hand refuses every purchase / pick, a merge-completing one too (PRTS 卫戍协议/帮助 §手牌区, GitHub #82)
if (handFull(priv)) return '整备区已满';
if (handFull(priv)) return t('整备区已满');
return null;
}
if (kind === 'refresh') return (Number(shop.refreshPrice) || 0) > funds ? '资金不足' : null;
if (kind === 'refresh') return (Number(shop.refreshPrice) || 0) > funds ? t('资金不足') : null;
if (kind === 'levelUp') {
if ((Number(shop.level) || 1) >= (Number(shop.maxLevel) || 6)) return '调度中心已达最高等级';
return (Number(shop.upgradePrice) || 0) > funds ? '资金不足' : null;
if ((Number(shop.level) || 1) >= (Number(shop.maxLevel) || 6)) return t('调度中心已达最高等级');
return (Number(shop.upgradePrice) || 0) > funds ? t('资金不足') : null;
}
return null;
}
@@ -177,7 +178,7 @@ export function readyFundsPrompt(priv, { ready = true, keptBands = null, autopla
const kept = Array.isArray(keptBands) ? keptBands : ['band_cannot'];
if (typeof priv.bandId === 'string' && kept.includes(priv.bandId)) return null;
return {
title: '剩余资金', micro: 'FUNDS LEFT', okText: '准备就绪', cancelText: '继续整备',
text: `还有 ${funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?`,
title: t('剩余资金'), micro: 'FUNDS LEFT', okText: t('准备就绪'), cancelText: t('继续整备'),
text: t('还有 {funds} 资金未使用。休整期结束时,本回合的剩余资金将清零。确定准备就绪吗?', { funds }),
};
}
+12 -11
View File
@@ -2,6 +2,7 @@
import { isObj, sortedPlayers } from './shared.js';
import { isCombatPhase } from './phases.js';
import { t } from '../../../../shared/i18n.js';
/** Own normal field id (DESIGN §8.3). */
@@ -47,8 +48,8 @@ export function cycleField(fields, current, dir = 1) {
* @returns {{ fieldId: string } | { reason: string }}
*/
export function watchTarget(p, pub, myId) {
if (!isObj(p)) return { reason: '无效的目标' };
if (p.alive === false) return { reason: '该队友已被淘汰,无法查看其阵地' };
if (!isObj(p)) return { reason: t('无效的目标') };
if (p.alive === false) return { reason: t('该队友已被淘汰,无法查看其阵地') };
const combat = isCombatPhase(pub?.phase);
const fieldId = (combat && typeof p.fieldId === 'string' && p.fieldId) || ownFieldId(p.playerId);
if (combat) {
@@ -57,7 +58,7 @@ export function watchTarget(p, pub, myId) {
const me = sortedPlayers(pub).find((x) => x.playerId === myId);
const mine = fields.find((x) => Array.isArray(x.players) && x.players.includes(myId));
if (f && (f.kind === 'boss' || f.kind === 'hidden') && me?.alive !== false && mine && mine.fieldId !== f.fieldId) {
return { reason: '无法查看另一组队友的战场' };
return { reason: t('无法查看另一组队友的战场') };
}
}
return { fieldId };
@@ -76,9 +77,9 @@ export function switcherLabel(pub, watching, myId, spectating = false) {
const cur = fields.find((f) => f.fieldId === watching);
if (cur) return fieldLabel(cur, pub, myId);
if (typeof watching === 'string' && watching.startsWith('n:') && watching !== ownFieldId(myId)) {
return sortedPlayers(pub).find((p) => p.playerId === watching.slice(2))?.name || '队友';
return sortedPlayers(pub).find((p) => p.playerId === watching.slice(2))?.name || t('队友');
}
return spectating ? '观战' : '自己';
return spectating ? t('观战') : t('自己');
}
/**
@@ -89,16 +90,16 @@ export function switcherLabel(pub, watching, myId, spectating = false) {
*/
export function fieldLabel(field, pub, myId) {
if (!isObj(field)) return '—';
const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || '博士']));
const names = new Map(sortedPlayers(pub).map((p) => [p.playerId, p.name || t('博士')]));
const ps = Array.isArray(field.players) ? field.players : [];
if (field.kind === 'unite') return ps.includes(myId) ? '联防(自己)' : '联防阵地';
if (field.kind === 'unite') return ps.includes(myId) ? t('联防(自己)') : t('联防阵地');
if (field.kind === 'boss' || field.kind === 'hidden') {
if (ps.includes(myId)) return ps.length > 1 ? '全景' : '自己';
return ps.map((id) => names.get(id) || '博士').join(' · ') || '领袖战场';
if (ps.includes(myId)) return ps.length > 1 ? t('全景') : t('自己');
return ps.map((id) => names.get(id) || t('博士')).join(' · ') || t('领袖战场');
}
if (ps.includes(myId) || field.fieldId === ownFieldId(myId)) return '自己';
if (ps.includes(myId) || field.fieldId === ownFieldId(myId)) return t('自己');
const id = ps[0] ?? String(field.fieldId || '').replace(/^n:/, '');
return names.get(id) || '队友';
return names.get(id) || t('队友');
}
/**
+2 -2
View File
@@ -111,8 +111,8 @@ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = fal
<${LpTower} value=${lp.lp} size="sm" tone=${Number.isFinite(lp.lp) && lp.lp - lp.pending <= 5 ? 'danger' : null} pending=${lp.pending}
tip=${rowLpTip(lp, cap)} />
${lp.left != null ? html`<${MissTag} n=${lp.left} name=${self ? null : p.name || t('博士')} />` : null}
<${Tooltip} text=${offline ? t('连接已断开') : meta.text} placement="right">
<span class=${cx('team__status', `is-${meta.tone}`, offline && 'is-offline', (offline || STATUS_SPRITE[status]) && localAsset('ui/battle', offline ? 'icon_lost_connect' : STATUS_SPRITE[status]) && 'has-sprite')} aria-label=${meta.text}>
<${Tooltip} text=${offline ? t('连接已断开') : t(meta.text)} placement="right">
<span class=${cx('team__status', `is-${meta.tone}`, offline && 'is-offline', (offline || STATUS_SPRITE[status]) && localAsset('ui/battle', offline ? 'icon_lost_connect' : STATUS_SPRITE[status]) && 'has-sprite')} aria-label=${t(meta.text)}>
${offline ? html`<${LocalSprite} name="icon_lost_connect" fallback=${html`<${Icon} name="wifiOff" />`} />`
: STATUS_SPRITE[status] ? html`<${LocalSprite} name=${STATUS_SPRITE[status]} fallback=${html`<${GIcon} name=${meta.glyph} />`} />`
: html`<${GIcon} name=${meta.glyph} />`}