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:
1 parent
647bd23ef2
commit
5741dc129b
10 files changed
+108
-85
No files matched your search
+7
-1
@@ -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?"
|
||||
}
|
||||
@@ -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('') }) : '';
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 -------------------------------------------------------------------------------------
|
||||
|
||||
@@ -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('无法放置在该位置');
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
@@ -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('队友');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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} />`}
|
||||
|
||||
Reference in new issue
Block a user